mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-14 22:20:17 +08:00
2711 lines
90 KiB
Python
2711 lines
90 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 json
|
|
import traceback
|
|
import binascii
|
|
import hashlib
|
|
import math
|
|
import keychain
|
|
import pybitcoin
|
|
import os
|
|
import copy
|
|
import shutil
|
|
|
|
from collections import defaultdict
|
|
from ..config import NAMESPACE_DEFAULT, MIN_OP_LENGTHS, OPCODES, MAX_NAMES_PER_SENDER, \
|
|
NAME_PREORDER, NAMESPACE_PREORDER, NAME_REGISTRATION, NAME_UPDATE, NAME_TRANSFER, TRANSFER_KEEP_DATA, \
|
|
TRANSFER_REMOVE_DATA, NAME_REVOKE, NAME_IMPORT, NAME_PREORDER_EXPIRE, \
|
|
NAMESPACE_PREORDER_EXPIRE, NAMESPACE_REVEAL_EXPIRE, NAMESPACE_REVEAL, BLOCKSTACK_VERSION, \
|
|
NAMESPACE_1_CHAR_COST, NAMESPACE_23_CHAR_COST, NAMESPACE_4567_CHAR_COST, NAMESPACE_8UP_CHAR_COST, NAME_COST_UNIT, \
|
|
TESTSET_NAMESPACE_1_CHAR_COST, TESTSET_NAMESPACE_23_CHAR_COST, TESTSET_NAMESPACE_4567_CHAR_COST, TESTSET_NAMESPACE_8UP_CHAR_COST, NAME_COST_UNIT, \
|
|
NAME_IMPORT_KEYRING_SIZE, GENESIS_SNAPSHOT, GENESIS_SNAPSHOT_TESTSET, default_blockstack_opts, NAMESPACE_READY, \
|
|
FIRST_BLOCK_MAINNET, FIRST_BLOCK_TESTNET, FIRST_BLOCK_MAINNET_TESTSET, FIRST_BLOCK_TESTNET_TESTSET, TESTNET, NAME_OPCODES
|
|
|
|
from ..operations import build_namespace_reveal, SERIALIZE_FIELDS
|
|
from ..hashing import *
|
|
from ..b40 import is_b40
|
|
|
|
import virtualchain
|
|
log = virtualchain.get_logger("blockstack-log")
|
|
|
|
# NOTE: ignored; here for compatibility with future versions
|
|
DISPOSITION_RO = "readonly"
|
|
DISPOSITION_RW = "readwrite"
|
|
|
|
|
|
class BlockstackDB( virtualchain.StateEngine ):
|
|
"""
|
|
State engine implementatoin for blockstack.
|
|
Tracks the set of names and namespaces, as well as the
|
|
latest hash of their profile data (which in turn resolves
|
|
to JSON in ancillary storage that contains the pointers
|
|
to their mutable data).
|
|
|
|
NOTE: this only works with small-ish datasets (~10 million names or less)
|
|
before things get too slow. At that point, we'll need
|
|
to upgrade to an actual database.
|
|
"""
|
|
|
|
def __init__(self, db_filename ):
|
|
"""
|
|
Construct a blockstack state engine, optionally from locally-cached
|
|
blockstack database state.
|
|
"""
|
|
|
|
import virtualchain_hooks
|
|
blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename() )
|
|
initial_snapshots = None
|
|
first_block = None
|
|
|
|
if blockstack_opts['testset']:
|
|
initial_snapshots = GENESIS_SNAPSHOT_TESTSET
|
|
if TESTNET:
|
|
first_block = FIRST_BLOCK_TESTNET_TESTSET
|
|
else:
|
|
first_block = FIRST_BLOCK_MAINNET_TESTSET
|
|
|
|
else:
|
|
initial_snapshots = GENESIS_SNAPSHOT
|
|
if TESTNET:
|
|
first_block = FIRST_BLOCK_TESTNET
|
|
else:
|
|
first_block = FIRST_BLOCK_MAINNET
|
|
|
|
|
|
super( BlockstackDB, self ).__init__( virtualchain_hooks.get_magic_bytes(), OPCODES, BlockstackDB.make_opfields(), impl=virtualchain_hooks, initial_snapshots=initial_snapshots, state=self )
|
|
|
|
self.firstblock = first_block
|
|
self.announce_ids = blockstack_opts['announcers'].split(",")
|
|
|
|
self.set_backup_frequency( blockstack_opts['backup_frequency'] )
|
|
self.set_backup_max_age( blockstack_opts['backup_max_age'] )
|
|
|
|
self.db_filename = db_filename
|
|
|
|
self.name_records = {} # map name.ns_id to a dict containing the name record
|
|
# in addition to containing all of the NAME_REC fields, it contains
|
|
# a 'history' dict that maps a block ID to the old values of fields changed at that block,
|
|
|
|
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_REVEAL op (a dict) combined with the namespace ID and sender script_pubkey
|
|
self.namespace_preorders = {} # map namespace ID hash (as the hex string of ns_id+script_pubkey hash) to its NAMESPACE_PREORDER operation
|
|
self.namespace_reveals = {} # map namesapce ID to its NAMESPACE_REVEAL operation
|
|
|
|
self.owner_names = defaultdict(list) # secondary index: map sender_script_pubkey hex string to list of names owned by the principal it represents
|
|
self.hash_names = {} # secondary index: map hex_hash160(name) to name
|
|
|
|
self.namespace_id_to_hash = {} # secondary index: map the namespace ID of a revealed namespace to its namespace hash. Entries here only exist until the namespace is ready
|
|
|
|
self.block_name_expires = defaultdict(list) # secondary index: map a block ID to the list of names that expire at that block.
|
|
|
|
self.name_consensus_hash_name = {} # secondary index: temporary table for mapping the hash(name + consensus_hash) in an update to its name
|
|
|
|
self.import_addresses = {} # secondary index: temporary table for mapping a NAMESPACE_REVEAL's root public key address to the set of all derived public key addresses.
|
|
# each NAME_IMPORT must come from one of these derived public key addresses.
|
|
|
|
self.address_names = defaultdict(list) # secondary index: map each address to the list of names registered to it.
|
|
# valid only for names where there is exactly one address (i.e. the sender is a p2pkh script)
|
|
|
|
# default namespace (empty string)
|
|
self.namespaces[""] = NAMESPACE_DEFAULT
|
|
self.namespaces[None] = NAMESPACE_DEFAULT
|
|
|
|
if db_filename:
|
|
try:
|
|
with open(db_filename, 'r') as f:
|
|
|
|
db_dict = json.loads(f.read())
|
|
|
|
if 'registrations' in db_dict:
|
|
self.name_records = db_dict['registrations']
|
|
|
|
if 'namespaces' in db_dict:
|
|
self.namespaces = db_dict['namespaces']
|
|
|
|
# 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 'namespace_preorders' in db_dict:
|
|
self.namespace_preorders = db_dict['namespace_preorders']
|
|
|
|
if 'namespace_reveals' in db_dict:
|
|
self.namespace_reveals = db_dict['namespace_reveals']
|
|
|
|
if 'preorders' in db_dict:
|
|
self.preorders = db_dict['preorders']
|
|
|
|
except Exception as e:
|
|
log.warning("Failed to open '%s'; creating a new database" % db_filename)
|
|
pass
|
|
|
|
for name, name_record in self.name_records.items():
|
|
|
|
# build up our reverse indexes on names
|
|
ns_id = get_namespace_from_name( name )
|
|
namespace = self.namespaces.get( ns_id, None )
|
|
if namespace is None:
|
|
# maybe its revealing?
|
|
namespace = self.namespace_reveals.get( ns_id, None )
|
|
if namespace is None:
|
|
raise Exception("Name with no namespace: '%s'" % name)
|
|
|
|
expires = name_record['last_renewed'] + namespace['lifetime']
|
|
|
|
# build expiration dates
|
|
if not self.block_name_expires.has_key( expires ):
|
|
self.block_name_expires[ expires ] = [name]
|
|
else:
|
|
self.block_name_expires[ expires ].append( name )
|
|
|
|
# build sender --> names
|
|
if not self.owner_names.has_key( name_record['sender'] ):
|
|
self.owner_names[ name_record['sender'] ] = [name]
|
|
else:
|
|
self.owner_names[ name_record['sender'] ].append( name )
|
|
|
|
# build hash --> name
|
|
self.hash_names[ hash256_trunc128( name ) ] = name
|
|
|
|
# build address --> names
|
|
if name_record.has_key('address'):
|
|
if not self.address_names.has_key( name_record['address'] ):
|
|
self.address_names[ name_record['address'] ] = [name]
|
|
else:
|
|
self.address_names[ name_record['address'] ].append( name )
|
|
|
|
# convert history to int
|
|
self.name_records[name]['history'] = BlockstackDB.sanitize_history( self.name_records[name]['history'] )
|
|
|
|
# convert vtxindex
|
|
self.name_records[name]['vtxindex'] = int(self.name_records[name]['vtxindex'])
|
|
|
|
|
|
for (namespace_id, namespace_reveal) in self.namespace_reveals.items():
|
|
|
|
# build up our reverse indexes on reveals
|
|
self.namespace_id_to_hash[ namespace_reveal['namespace_id'] ] = namespace_reveal['namespace_id_hash']
|
|
|
|
if namespace_id in (None, ""):
|
|
continue
|
|
|
|
pubkey_hex = None
|
|
pubkey_addr = None
|
|
|
|
# find a revealed name whose sender's address matches the namespace recipient's
|
|
for name, name_record in self.name_records.items():
|
|
if not name.endswith( namespace_id ):
|
|
continue
|
|
|
|
if not name_record.has_key('sender_pubkey'):
|
|
continue
|
|
|
|
pubkey_hex = name_record['sender_pubkey']
|
|
pubkey_addr = pybitcoin.BitcoinPublicKey( str(pubkey_hex) ).address()
|
|
|
|
if pubkey_addr != namespace_reveal['recipient_address']:
|
|
continue
|
|
|
|
break
|
|
|
|
if pubkey_hex is not None:
|
|
log.debug("Deriving %s children of %s ('%s') for '%s'" % (NAME_IMPORT_KEYRING_SIZE, pubkey_addr, pubkey_hex, namespace_id))
|
|
|
|
# generate all possible addresses from this public key
|
|
self.import_addresses[ namespace_id ] = BlockstackDB.build_import_keychain( pubkey_hex )
|
|
|
|
# convert history to int
|
|
self.namespace_reveals[namespace_id]['history'] = BlockstackDB.sanitize_history( namespace_reveal['history'] )
|
|
|
|
for (namespace_id, namespace) in self.namespaces.items():
|
|
|
|
# sanitize history on import
|
|
self.namespaces[namespace_id]['history'] = BlockstackDB.sanitize_history( namespace['history'] )
|
|
|
|
self.prescanned = False
|
|
|
|
|
|
@classmethod
|
|
def make_opfields( cls ):
|
|
"""
|
|
Calculate the virtulachain-required opfields dict.
|
|
"""
|
|
# construct fields
|
|
opfields = {}
|
|
for opname in SERIALIZE_FIELDS.keys():
|
|
opcode = NAME_OPCODES[opname]
|
|
opfields[opcode] = SERIALIZE_FIELDS[opname]
|
|
|
|
return opfields
|
|
|
|
|
|
def save_db(self, filename):
|
|
"""
|
|
Cache the set of blockstack operations to disk,
|
|
so we don't have to go build them up again from
|
|
the blockchain.
|
|
"""
|
|
|
|
try:
|
|
with open(filename, 'w') as f:
|
|
|
|
db_dict = {
|
|
'registrations': self.name_records,
|
|
'preorders': self.preorders,
|
|
'namespaces': self.namespaces,
|
|
'namespace_preorders': self.namespace_preorders,
|
|
'namespace_reveals': self.namespace_reveals
|
|
}
|
|
|
|
f.write(json.dumps(db_dict))
|
|
f.flush()
|
|
|
|
except Exception as e:
|
|
log.exception(e)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def get_db_path( self ):
|
|
"""
|
|
Get db file path
|
|
"""
|
|
return self.db_filename
|
|
|
|
|
|
def export_db( self, path ):
|
|
"""
|
|
Copy the database to the given location.
|
|
"""
|
|
shutil.copyfile( self.get_db_path(), path )
|
|
|
|
|
|
@classmethod
|
|
def sanitize_history( cls, history ):
|
|
"""
|
|
Given a record's history dict, sanitize it:
|
|
* convert string-ified block numbers to ints.
|
|
"""
|
|
|
|
block_number_strs = history.keys()
|
|
for block_number_str in block_number_strs:
|
|
history_rec = history[block_number_str]
|
|
del history[block_number_str]
|
|
history[int(block_number_str)] = history_rec
|
|
|
|
return history
|
|
|
|
|
|
@classmethod
|
|
def build_import_keychain( cls, pubkey_hex ):
|
|
"""
|
|
Generate all possible NAME_IMPORT addresses from the NAMESPACE_REVEAL public key
|
|
"""
|
|
|
|
pubkey_addr = pybitcoin.BitcoinPublicKey( str(pubkey_hex) ).address()
|
|
|
|
# do we have a cached one on disk?
|
|
cached_keychain = os.path.join( virtualchain.get_working_dir(), "%s.keychain" % pubkey_addr)
|
|
if os.path.exists( cached_keychain ):
|
|
|
|
child_addrs = []
|
|
try:
|
|
lines = []
|
|
with open(cached_keychain, "r") as f:
|
|
lines = f.readlines()
|
|
|
|
child_attrs = [l.strip() for l in lines]
|
|
|
|
log.debug("Loaded cached import keychain for '%s' (%s)" % (pubkey_hex, pubkey_addr))
|
|
return child_attrs
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
pass
|
|
|
|
pubkey_hex = str(pubkey_hex)
|
|
public_keychain = keychain.PublicKeychain.from_public_key( pubkey_hex )
|
|
child_addrs = []
|
|
|
|
for i in xrange(0, NAME_IMPORT_KEYRING_SIZE):
|
|
public_child = public_keychain.child(i)
|
|
public_child_address = public_child.address()
|
|
|
|
child_addrs.append( public_child_address )
|
|
|
|
if i % 20 == 0 and i != 0:
|
|
log.debug("%s children..." % i)
|
|
|
|
# include this address
|
|
child_addrs.append( pubkey_addr )
|
|
|
|
log.debug("Done building import keychain for '%s' (%s)" % (pubkey_hex, pubkey_addr))
|
|
|
|
# cache
|
|
try:
|
|
with open(cached_keychain, "w+") as f:
|
|
for addr in child_addrs:
|
|
f.write("%s\n" % addr)
|
|
|
|
f.flush()
|
|
|
|
log.debug("Cached keychain to '%s'" % cached_keychain)
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("Unable to cache keychain for '%s' (%s)" % (pubkey_hex, pubkey_addr))
|
|
|
|
return child_addrs
|
|
|
|
|
|
def is_name_expired( self, name, block_number ):
|
|
"""
|
|
Given a name, determine if it is expired.
|
|
* names in revealed but not ready namespaces are never expired
|
|
* names in ready namespaces expire once max(ready_block, renew_block) + lifetime blocks passes
|
|
|
|
Return True if so
|
|
Return False if not, or if the name doesn't exist
|
|
"""
|
|
|
|
namerec = self.name_records.get( name, None )
|
|
if namerec is None:
|
|
# doesn't exist
|
|
return False
|
|
|
|
ns_id = get_namespace_from_name( name )
|
|
ns = self.get_namespace( ns_id )
|
|
if ns is None:
|
|
# maybe revealed?
|
|
ns = self.get_namespace_reveal( ns_id )
|
|
if ns is None:
|
|
# doesn't exist
|
|
return True
|
|
else:
|
|
# imported into non-ready namespace
|
|
return False
|
|
|
|
else:
|
|
if max( ns['ready_block'], namerec['last_renewed'] ) + ns['lifetime'] < block_number:
|
|
# expired
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def get_name( self, name, include_expired=False ):
|
|
"""
|
|
Given a name, return the latest version and history of
|
|
the metadata gleaned from the blockchain.
|
|
Name must be fully-qualified (i.e. name.ns_id)
|
|
Return None if no such name is registered.
|
|
"""
|
|
|
|
if name not in self.name_records.keys():
|
|
return None
|
|
|
|
else:
|
|
# don't return expired names
|
|
if not include_expired and self.is_name_expired( name, self.lastblock ):
|
|
return None
|
|
|
|
else:
|
|
return self.name_records[name]
|
|
|
|
|
|
def get_name_at( self, name, block_number ):
|
|
"""
|
|
Generate and return the sequence of of states a name record was in
|
|
at a particular block number.
|
|
"""
|
|
|
|
name_rec = self.get_name( name )
|
|
|
|
# trivial reject
|
|
if name_rec is None:
|
|
# never existed
|
|
return None
|
|
|
|
if block_number < name_rec['block_number']:
|
|
# didn't exist then
|
|
return None
|
|
|
|
historical_recs = BlockstackDB.restore_from_history( name_rec, block_number )
|
|
return historical_recs
|
|
|
|
|
|
def get_namespace_at( self, namespace_id, block_number ):
|
|
"""
|
|
Generate and return the sequence of states a namespace record was in
|
|
at a particular block number.
|
|
"""
|
|
|
|
namespace_rec = self.get_namespace( namespace_id )
|
|
if namespace_rec is None:
|
|
return None
|
|
|
|
if block_number < namespace_rec['block_number']:
|
|
return None
|
|
|
|
historical_recs = BlockstackDB.restore_from_history( namespace_rec, block_number )
|
|
return historical_recs
|
|
|
|
|
|
def get_name_history( self, name, start_block, end_block ):
|
|
"""
|
|
Get the sequence of states of a name over a given point in time.
|
|
|
|
TODO: this is not particularly efficient
|
|
"""
|
|
|
|
name_rec = self.get_name( name )
|
|
if name_rec is None:
|
|
return None
|
|
|
|
name_snapshots = []
|
|
|
|
update_points = sorted( name_rec['history'].keys() )
|
|
for update_point in update_points:
|
|
if update_point >= start_block and update_point < end_block:
|
|
historical_recs = self.get_name_at( name, update_point )
|
|
name_snapshots += historical_recs
|
|
|
|
return name_snapshots
|
|
|
|
|
|
def get_names_owned_by_address( self, address ):
|
|
"""
|
|
Get the set of names owned by a particular address.
|
|
Only valid if the name was sent by a p2pkh script.
|
|
"""
|
|
|
|
if self.address_names.has_key( address ):
|
|
return self.address_names[address]
|
|
else:
|
|
return None
|
|
|
|
|
|
def _rec_dup( self, rec ):
|
|
"""
|
|
(private method)
|
|
Duplicate a name record, and strip out all
|
|
unnecessary information.
|
|
"""
|
|
ret = copy.deepcopy( rec )
|
|
del ret['history']
|
|
return ret
|
|
|
|
|
|
def get_all_nameops_at( self, block_id ):
|
|
"""
|
|
Given a block number, get the set of sequences name operations
|
|
created or altered at that block number.
|
|
|
|
Return the list of names, in the order their transactions occurred.
|
|
"""
|
|
|
|
ret = []
|
|
|
|
# all name records
|
|
for (name, name_rec) in self.name_records.items():
|
|
if block_id < name_rec['block_number'] or block_id not in (name_rec['history'].keys() + [name_rec['block_number']]):
|
|
# neither created nor altered at this block
|
|
continue
|
|
|
|
recs = BlockstackDB.restore_from_history( name_rec, block_id )
|
|
ret += recs
|
|
|
|
# all current preorders
|
|
for (name_hash, preorder) in self.preorders.items():
|
|
if block_id == preorder['block_number']:
|
|
|
|
rec = self._rec_dup( preorder )
|
|
ret.append( rec )
|
|
|
|
# all namespaces
|
|
for (namespace_id, namespace) in self.namespaces.items():
|
|
|
|
# null namespaces don't exist
|
|
if namespace_id is None or len(namespace_id) == 0:
|
|
continue
|
|
|
|
if block_id < namespace['block_number'] or block_id not in (namespace['history'].keys() + [namespace['block_number']]):
|
|
# neither created nor altered at this block
|
|
continue
|
|
|
|
recs = BlockstackDB.restore_from_history( namespace, block_id )
|
|
ret += recs
|
|
|
|
# all current namespace preorders
|
|
for (namespace_id_hash, namespace_preorder) in self.namespace_preorders.items():
|
|
if block_id == namespace_preorder['block_number']:
|
|
|
|
rec = self._rec_dup( namespace_preorder )
|
|
ret.append( rec )
|
|
|
|
# all current namespace reveals
|
|
for (namespace_id, namespace_reveal) in self.namespace_reveals.items():
|
|
|
|
if block_id < namespace_reveal['block_number'] or block_id not in namespace_reveal['history'].keys() + [namespace_reveal['block_number']]:
|
|
continue
|
|
|
|
recs = BlockstackDB.restore_from_history( namespace_reveal, block_id )
|
|
ret += recs
|
|
|
|
return sorted( ret, key=lambda n: n['vtxindex'] )
|
|
|
|
|
|
def get_all_names( self, offset=None, count=None ):
|
|
"""
|
|
Get the set of all registered names, with optional pagination
|
|
Returns the list of names.
|
|
TODO: this is somewhat inefficient with offsets, since we have
|
|
to sort the name set first.
|
|
"""
|
|
|
|
if offset is None:
|
|
offset = 0
|
|
|
|
if offset < 0:
|
|
raise Exception("Invalid offset %s" % offset)
|
|
|
|
if offset >= len(self.name_records.keys()):
|
|
return []
|
|
|
|
names = []
|
|
if count is None:
|
|
names = self.name_records.keys()[:]
|
|
names.sort()
|
|
|
|
else:
|
|
names = sorted(self.name_records.keys())[offset:min(offset+count, len(self.name_records.keys()))]
|
|
names.sort()
|
|
|
|
#return dict( zip( names, [self.name_records[name] for name in names] ) )
|
|
return names
|
|
|
|
def get_names_in_namespace( self, namespace_id, offset=None, count=None ):
|
|
"""
|
|
Get the current set of all registered names in a particular namespace
|
|
TODO: this is somewhat inefficient since we have to scan through
|
|
the whole name set.
|
|
"""
|
|
|
|
if offset is None:
|
|
offset = 0
|
|
|
|
if offset < 0:
|
|
raise Exception("Invalid offset %s" % offset)
|
|
|
|
if offset >= len(self.name_records.keys()):
|
|
return []
|
|
|
|
all_names = self.name_records.keys()[:]
|
|
all_names.sort()
|
|
|
|
namespace_names = []
|
|
|
|
for name in all_names:
|
|
if get_namespace_from_name( name ) != namespace_id:
|
|
continue
|
|
|
|
if offset == 0:
|
|
namespace_names.append( name )
|
|
if len(namespace_names) >= count:
|
|
break
|
|
|
|
else:
|
|
offset -= 1
|
|
|
|
data = {}
|
|
data['results'] = namespace_names
|
|
|
|
# old format that returned data on individual records as well
|
|
#return dict( zip( namespace_names, [self.name_records[name] for name in namespace_names] ) )
|
|
return data
|
|
|
|
|
|
|
|
def get_namespace( self, namespace_id ):
|
|
"""
|
|
Given a namespace ID, get the namespace op for it.
|
|
|
|
Return the dict with the parameters on success.
|
|
Return None if the namespace does not exist.
|
|
"""
|
|
|
|
return self.namespaces.get( namespace_id, None )
|
|
|
|
|
|
def get_all_namespace_ids( self ):
|
|
"""
|
|
Get the set of all namespace IDs
|
|
"""
|
|
|
|
return self.namespaces.keys()
|
|
|
|
|
|
def get_all_preordered_namespace_hashes( self ):
|
|
"""
|
|
Get all outstanding namespace hashes
|
|
"""
|
|
namespace_hashes = self.namespace_preorders.keys()
|
|
ret = []
|
|
for nh in namespace_hashes:
|
|
if self.namespace_preorders[nh]['block_number'] + NAMESPACE_PREORDER_EXPIRE > self.lastblock:
|
|
# not expired
|
|
ret.append( nh )
|
|
|
|
return ret
|
|
|
|
|
|
def get_all_revealed_namespace_ids( self ):
|
|
"""
|
|
Get the IDs of all outsanding revealed namespaces.
|
|
"""
|
|
namespace_ids = self.namespace_reveals.keys()
|
|
ret = []
|
|
for nsid in namespace_ids:
|
|
if self.namespace_reveals[nsid]['reveal_block'] + NAMESPACE_REVEAL_EXPIRE > self.lastblock:
|
|
# not expired
|
|
ret.append( nsid )
|
|
|
|
return ret
|
|
|
|
|
|
def get_all_importing_namespace_hashes( self ):
|
|
"""
|
|
Get the set of all preordered and revealed namespace hashes.
|
|
"""
|
|
|
|
revealed_namespace_hashes = []
|
|
for (namespace_id, revealed_namespace) in self.namespace_reveals.items():
|
|
|
|
# skip expired namespace reveals
|
|
if self.namespace_reveals[namespace_id]['expired']:
|
|
continue
|
|
|
|
revealed_namespace_hashes.append( revealed_namespace['namespace_id_hash'] )
|
|
|
|
return self.namespace_preorders.keys() + revealed_namespace_hashes
|
|
|
|
|
|
def get_name_from_name_consensus_hash( self, name_consensus_hash, sender, block_id ):
|
|
"""
|
|
Find the name.ns_id from hash( name.ns_id, consensus_hash ), given the sender and block_id,
|
|
and assuming that name.ns_id is already registered.
|
|
There are only a small number of possible values this can take, so test them all to see
|
|
if the hash matches one of them.
|
|
|
|
This is useful for name updates--we need to ensure that updates are timely, and on
|
|
the majority fork of the blockchain.
|
|
|
|
Return the (fully-qualified name, consensus hash) on success
|
|
Return (None, None) if not found
|
|
"""
|
|
|
|
names = self.owner_names.get( sender, None )
|
|
if names is None:
|
|
|
|
log.error("Sender %s owns no names" % sender)
|
|
# invalid name owner
|
|
return None, None
|
|
|
|
possible_consensus_hashes = []
|
|
|
|
for i in xrange( block_id - virtualchain.config.BLOCKS_CONSENSUS_HASH_IS_VALID, block_id+1 ):
|
|
consensus_hash = self.get_consensus_at( i )
|
|
if consensus_hash is not None and consensus_hash not in possible_consensus_hashes:
|
|
possible_consensus_hashes.append( str(consensus_hash) )
|
|
|
|
for name in names:
|
|
for consensus_hash in possible_consensus_hashes:
|
|
|
|
# what would have been the name/consensus_hash?
|
|
test_name_consensus_hash = hash256_trunc128( str(name) + consensus_hash )
|
|
if test_name_consensus_hash == name_consensus_hash:
|
|
|
|
# found!
|
|
return name, consensus_hash
|
|
|
|
return None, None
|
|
|
|
|
|
def get_name_preorder( self, name, sender_script_pubkey, register_addr ):
|
|
"""
|
|
Get the preorder for a name, given the name, the sender's script_pubkey string, and the
|
|
registration address used to calculate the preorder hash.
|
|
Return the nameop on success
|
|
Return None if not found, or if the preorder is expired, or if the preorder is already registered.
|
|
"""
|
|
|
|
# name registered and not expired?
|
|
name_rec = self.get_name( name )
|
|
if name_rec is not None:
|
|
return None
|
|
|
|
preorder_hash = hash_name(name, sender_script_pubkey, register_addr=register_addr)
|
|
if preorder_hash not in self.preorders.keys():
|
|
return None
|
|
|
|
else:
|
|
return self.preorders[ preorder_hash ]
|
|
|
|
|
|
def get_namespace_preorder( self, namespace_id_hash ):
|
|
"""
|
|
Given the hash(namespace_id, sender_script_pubkey) for a namespace that is
|
|
being imported, get its assocated NAMESPACE_PREORDER operation.
|
|
|
|
Return the op as a dict on success
|
|
Return None on error
|
|
"""
|
|
|
|
if namespace_id_hash not in self.namespace_preorders.keys():
|
|
return None
|
|
|
|
return self.namespace_preorders[ namespace_id_hash ]
|
|
|
|
|
|
def get_name_owner( self, name ):
|
|
"""
|
|
Get the script_pubkey hex string of the user that
|
|
owns the given name.
|
|
|
|
Return the string on success
|
|
Return None if not found.
|
|
"""
|
|
if name in self.name_records.keys() and 'sender' in self.name_records[name]:
|
|
return self.name_records[name]['sender']
|
|
|
|
else:
|
|
return None
|
|
|
|
|
|
def get_namespace_reveal( self, namespace_id ):
|
|
"""
|
|
Given the name of a namespace, go get its NAMESPACE_REVEAL operation.
|
|
Check the set of readied namespaces, and then check the list of
|
|
pending imports.
|
|
|
|
Return the nameop on success
|
|
Return None if not found
|
|
"""
|
|
|
|
return self.namespace_reveals.get( namespace_id, None )
|
|
|
|
|
|
def get_names_with_value_hash( self, value_hash ):
|
|
"""
|
|
Find the list of names that have the given value hash.
|
|
Omit expired or revoked names.
|
|
"""
|
|
ret = []
|
|
for name in self.name_records.keys():
|
|
|
|
rec = self.name_records[name]
|
|
|
|
# revoked?
|
|
if rec.has_key('revoked') and rec['revoked']:
|
|
continue
|
|
|
|
# expired?
|
|
if self.is_name_expired( rec['name'], self.lastblock ):
|
|
continue
|
|
|
|
if rec.has_key('value_hash') and rec['value_hash'] == value_hash:
|
|
ret.append(rec['name'])
|
|
|
|
return ret
|
|
|
|
|
|
@classmethod
|
|
def flatten_history( cls, hist ):
|
|
"""
|
|
Given a name's history, flatten it into a list of deltas.
|
|
They will be in *increasing* order.
|
|
"""
|
|
ret = []
|
|
block_ids = sorted(hist.keys())
|
|
for block_id in block_ids:
|
|
vtxinfos = hist[block_id]
|
|
for vtxinfo in vtxinfos:
|
|
info = copy.deepcopy(vtxinfo)
|
|
ret.append(info)
|
|
|
|
return ret
|
|
|
|
|
|
def get_name_value_hash_txid( self, name, value_hash ):
|
|
"""
|
|
Given the name and value hash, find the txid that put it.
|
|
Omit if expired or revoked (return None)
|
|
"""
|
|
rec = self.get_name( name )
|
|
if rec is None:
|
|
return None
|
|
|
|
if rec.has_key('revoked') and rec['revoked']:
|
|
return None
|
|
|
|
if self.is_name_expired(rec['name'], self.lastblock ):
|
|
return None
|
|
|
|
# current?
|
|
if rec['value_hash'] == value_hash:
|
|
return rec['txid']
|
|
|
|
# search history, backwards
|
|
hist = rec['history']
|
|
flat_hist = BlockstackDB.flatten_history( hist )
|
|
|
|
for i in xrange(len(flat_hist)-1, 0, -1):
|
|
delta = flat_hist[i]
|
|
if delta.has_key('op') and delta['op'] == NAME_PREORDER:
|
|
# this name was re-registered. skip
|
|
return None
|
|
|
|
if delta.has_key('value_hash') and delta['value_hash'] == value_hash:
|
|
# this is the txid that affected it
|
|
return delta['txid']
|
|
|
|
# not found
|
|
return None
|
|
|
|
|
|
def find_expiring_at( self, block_id ):
|
|
"""
|
|
Find all names that will expire at a particular block.
|
|
|
|
Returns a list of names on success (which can be empty)
|
|
"""
|
|
return self.block_name_expires.get( block_id, [] )
|
|
|
|
|
|
def is_name_registered(self, name):
|
|
"""
|
|
Is the given fully-qualified name (name.ns_id) registered and available?
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
|
|
if name not in self.name_records.keys():
|
|
return False
|
|
|
|
if self.name_records[name].has_key('revoked') and self.name_records[name]['revoked']:
|
|
return False
|
|
|
|
if self.is_name_expired( name, self.lastblock ):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def is_namespace_ready( self, namespace_id ):
|
|
"""
|
|
Has a namepace with the given human-readable ID been declared?
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
return namespace_id in self.namespaces.keys()
|
|
|
|
|
|
def is_namespace_preordered( self, namespace_id_hash ):
|
|
"""
|
|
Has the given namespace been preordered?
|
|
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
|
|
namespace_preorder = self.get_namespace_preorder( namespace_id_hash )
|
|
if namespace_preorder is None:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def is_namespace_revealed( self, namespace_id ):
|
|
"""
|
|
Given the name of a namespace, has it been revealed yet but not yet readied?
|
|
|
|
Return True if so
|
|
Return False if not.
|
|
"""
|
|
return self.get_namespace_reveal( namespace_id ) is not None
|
|
|
|
|
|
def is_name_owner( self, name, sender_script_pubkey ):
|
|
"""
|
|
Given the fully-qualified name (name.ns_id) and a list of senders'
|
|
script_pubkey hex strings, see if the sender is the name owner.
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
if name in self.name_records.keys() and 'sender' in self.name_records[name]:
|
|
if self.name_records[name]['sender'] == sender_script_pubkey:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_new_preorder( self, name_hash ):
|
|
"""
|
|
Given the preorder hash of a name, determine whether or not the name is in the process of being ordered already.
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
return (name_hash not in self.preorders.keys())
|
|
|
|
|
|
def is_new_namespace_preorder( self, namespace_id_hash ):
|
|
"""
|
|
Given the preorder hash of a namespace's ID, determine whether or not the namespace is in the process of being ordered already.
|
|
|
|
Return True if so.
|
|
Return False if not.
|
|
"""
|
|
return (self.get_namespace_preorder(namespace_id_hash) is None)
|
|
|
|
|
|
def is_name_revoked( self, name ):
|
|
"""
|
|
Given a name, is it revoked?
|
|
|
|
Return True if so
|
|
Return False if not, or if the name does not exist
|
|
"""
|
|
|
|
if not name in self.name_records.keys():
|
|
return False
|
|
|
|
return self.name_records[name]['revoked']
|
|
|
|
|
|
def lookup_block_id_from_consensus_hash( self, consensus_hash ):
|
|
"""
|
|
Given a consensus hash, find the matching block ID
|
|
Return None if not found
|
|
"""
|
|
for i in xrange(self.lastblock, self.firstblock, -1):
|
|
ch = self.get_consensus_at(i)
|
|
if ch == consensus_hash:
|
|
return i
|
|
|
|
return None
|
|
|
|
|
|
@classmethod
|
|
def restore_from_history( cls, rec, block_number ):
|
|
"""
|
|
Given a name or a namespace record, replay its
|
|
history diffs "back in time" to a particular block
|
|
number.
|
|
|
|
Return the sequence of states the name record went
|
|
through at that block number, starting from the beginning
|
|
of the block.
|
|
|
|
Return None if the record does not exist at that point in time
|
|
|
|
The returned records will *not* have a 'history' key.
|
|
"""
|
|
|
|
block_history = list( reversed( sorted( rec['history'].keys() ) ) )
|
|
|
|
historical_rec = copy.deepcopy( rec )
|
|
del historical_rec['history']
|
|
|
|
if block_number > block_history[0]:
|
|
# current record is valid
|
|
return historical_rec
|
|
|
|
if block_number < rec['block_number']:
|
|
# doesn't yet exist
|
|
return None
|
|
|
|
# find the latest block prior to block_number
|
|
last_block = len(block_history)
|
|
for i in xrange( 0, len(block_history) ):
|
|
if block_number >= block_history[i]:
|
|
last_block = i
|
|
break
|
|
|
|
i = 0
|
|
while i < last_block:
|
|
|
|
try:
|
|
diff_list = list( reversed( rec['history'][ block_history[i] ] ) )
|
|
except:
|
|
print json.dumps( rec['history'][block_history[i]] )
|
|
raise
|
|
|
|
for diff in diff_list:
|
|
|
|
if diff.has_key('history_snapshot'):
|
|
# wholly new state
|
|
historical_rec = copy.deepcopy( diff )
|
|
del historical_rec['history_snapshot']
|
|
|
|
else:
|
|
# delta in current state
|
|
# no matter what, 'block_number' cannot be altered (unless it's a history snapshot)
|
|
if diff.has_key('block_number'):
|
|
del diff['block_number']
|
|
|
|
historical_rec.update( diff )
|
|
|
|
i += 1
|
|
|
|
# if this isn't the earliest history element, and the next-earliest
|
|
# one (at last block) has multiple entries, then generate the sequence
|
|
# of updates for all but the first one. This is because all but the
|
|
# first one were generated in the same block (i.e. the block requested).
|
|
updates = [ copy.deepcopy( historical_rec ) ]
|
|
|
|
if i < len(block_history):
|
|
|
|
try:
|
|
diff_list = list( reversed( rec['history'][ block_history[i] ] ) )
|
|
except:
|
|
print json.dumps( rec['history'][block_history[i]] )
|
|
raise
|
|
|
|
if len(diff_list) > 1:
|
|
for diff in diff_list[:-1]:
|
|
|
|
# no matter what, 'block_number' cannot be altered
|
|
if diff.has_key('block_number'):
|
|
del diff['block_number']
|
|
|
|
if diff.has_key('history_snapshot'):
|
|
# wholly new state
|
|
historical_rec = copy.deepcopy( diff )
|
|
del historical_rec['history_snapshot']
|
|
|
|
else:
|
|
# delta in current state
|
|
historical_rec.update( diff )
|
|
|
|
updates.append( copy.deepcopy(historical_rec) )
|
|
|
|
return list( reversed( updates ) )
|
|
|
|
|
|
@classmethod
|
|
def save_diff( self, rec, block_id, field_list ):
|
|
"""
|
|
Back up a set of fields that will change for a given record.
|
|
Update the record to include the modified fields in its
|
|
'history' dict. Add the 'history' dict if it doesn't exist.
|
|
|
|
NOTE: only call this *once* per record--records should only
|
|
be updated at most once per block!
|
|
"""
|
|
|
|
diff_rec = {}
|
|
for field in field_list:
|
|
if field in rec:
|
|
diff_rec[field] = copy.deepcopy( rec[field] )
|
|
|
|
if not rec.has_key('history'):
|
|
rec['history'] = {}
|
|
|
|
if rec['history'].has_key( block_id ):
|
|
rec['history'][block_id].append( diff_rec )
|
|
|
|
else:
|
|
rec['history'][block_id] = [diff_rec]
|
|
|
|
return rec
|
|
|
|
|
|
def save_name_diff( self, name, block_id, field_list):
|
|
"""
|
|
Back up a set of fields that will change for a name record.
|
|
This is to be done whenever the name undergoes a state change,
|
|
so we can later reconstruct the name at a particular block ID.
|
|
"""
|
|
|
|
if not self.name_records.has_key(name):
|
|
return False
|
|
|
|
self.name_records[name] = BlockstackDB.save_diff( self.name_records[name], block_id, field_list )
|
|
|
|
|
|
def commit_name_expire( self, name, block_id ):
|
|
"""
|
|
Remove an expired name.
|
|
The caller must verify that the expiration criteria have been met.
|
|
Return True if expired
|
|
Return False if not
|
|
"""
|
|
|
|
name_hash = hash256_trunc128( name )
|
|
|
|
owner = None
|
|
|
|
if not self.name_records.has_key( name ):
|
|
return False
|
|
|
|
# anyone can claim the name now
|
|
self.name_records[name]['revoked'] = False
|
|
|
|
owner = self.name_records[name].get('sender', None)
|
|
address = self.name_records[name].get('address', None)
|
|
|
|
# update secondary indexes
|
|
if owner is not None and self.owner_names.has_key( owner ) and name in self.owner_names[owner]:
|
|
self.owner_names[ owner ].remove( name )
|
|
if len(self.owner_names[owner]) == 0:
|
|
del self.owner_names[owner]
|
|
|
|
if address is not None and self.address_names.has_key( address ) and name in self.address_names[address]:
|
|
self.address_names[ address ].remove( name )
|
|
if len(self.address_names[address]) == 0:
|
|
del self.address_names[address]
|
|
|
|
if self.hash_names.has_key( name_hash ):
|
|
del self.hash_names[ name_hash ]
|
|
|
|
return True
|
|
|
|
|
|
def commit_preorder_expire_all( self, block_id ):
|
|
"""
|
|
Given the block ID, go find and remove all expired preorders
|
|
|
|
Return the list of expired name hashes
|
|
"""
|
|
|
|
expired = []
|
|
for (name_hash, nameop) in self.preorders.items():
|
|
if nameop['block_number'] + NAME_PREORDER_EXPIRE == block_id:
|
|
# expired
|
|
log.debug("Expire name preorder '%s'" % name_hash)
|
|
expired.append( name_hash )
|
|
|
|
return expired
|
|
|
|
|
|
def commit_namespace_preorder_expire_all( self, block_id ):
|
|
"""
|
|
Given the block ID, go find and remove all expired namespace preorders
|
|
|
|
Return the list of expired namespace hashes
|
|
"""
|
|
|
|
expired = []
|
|
for (namespace_id_hash, preorder_nameop) in self.namespace_preorders.items():
|
|
|
|
if preorder_nameop['block_number'] + NAMESPACE_PREORDER_EXPIRE == block_id:
|
|
# expired
|
|
log.debug("Expire namespace preorder '%s'" % namespace_id_hash)
|
|
expired.append( namespace_id_hash )
|
|
|
|
return expired
|
|
|
|
|
|
def commit_namespace_reveal_expire_all( self, block_id ):
|
|
"""
|
|
Given the block ID, go find and remove all expired namespace reveals
|
|
that have not been made ready. Remove their associated name imports.
|
|
|
|
Return a dict that maps an expired namespace_id to the list of names expired.
|
|
"""
|
|
|
|
expired = {}
|
|
for (namespace_id, reveal_op) in self.namespace_reveals.items():
|
|
|
|
if reveal_op['block_number'] + NAMESPACE_REVEAL_EXPIRE == block_id:
|
|
|
|
# expired
|
|
log.debug("Expire incomplete namespace '%s'" % namespace_id)
|
|
del self.namespace_reveals[ namespace_id ]
|
|
del self.namespace_id_to_hash[ namespace_id ]
|
|
|
|
if namespace_id in self.import_addresses.keys():
|
|
del self.import_addresses[ namespace_id ]
|
|
|
|
expired[namespace_id] = []
|
|
|
|
for name in self.name_records.keys():
|
|
|
|
if namespace_id == get_namespace_from_name( name ):
|
|
# part of this namespace
|
|
log.debug("Expire imported name '%s'" % name)
|
|
self.commit_name_expire( name, block_id )
|
|
|
|
expired[namespace_id].append( name )
|
|
|
|
return expired
|
|
|
|
|
|
|
|
def commit_name_expire_all( self, block_id ):
|
|
"""
|
|
Given a block ID, remove all name records that expired
|
|
at this block.
|
|
|
|
Return the list of expired names.
|
|
"""
|
|
|
|
expired_names = self.find_expiring_at( block_id )
|
|
for name in expired_names:
|
|
log.debug("Expire name '%s'" % name)
|
|
self.commit_name_expire( name, block_id )
|
|
|
|
if block_id in self.block_name_expires.keys():
|
|
del self.block_name_expires[ block_id ]
|
|
|
|
return expired_names
|
|
|
|
|
|
def commit_remove_preorder( self, name, script_pubkey, register_addr ):
|
|
"""
|
|
Given the fully-qualified name (name.ns_id) and a script_pubkey hex string,
|
|
remove the preorder.
|
|
|
|
Return the old preorder
|
|
"""
|
|
try:
|
|
name_hash = hash_name(name, script_pubkey, register_addr=register_addr)
|
|
except ValueError:
|
|
return None
|
|
else:
|
|
if self.preorders.has_key(name_hash):
|
|
old_preorder = self.preorders[name_hash]
|
|
del self.preorders[name_hash]
|
|
return old_preorder
|
|
|
|
else:
|
|
log.error("BUG: No preorder found for '%s' from '%s'" % (name, script_pubkey))
|
|
raise Exception("BUG: no preorder found for '%s' from '%s'" % (name, script_pubkey))
|
|
|
|
|
|
def commit_remove_namespace_import( self, namespace_id, namespace_id_hash ):
|
|
"""
|
|
Given the namespace ID, go remove the namespace preorder and reveal.
|
|
(i.e. call this on a NAMESPACE_READY commit).
|
|
"""
|
|
|
|
if self.namespace_preorders.has_key( namespace_id_hash ):
|
|
del self.namespace_preorders[ namespace_id_hash ]
|
|
|
|
if self.namespace_reveals.has_key( namespace_id ):
|
|
del self.namespace_reveals[ namespace_id ]
|
|
|
|
if self.namespace_id_to_hash.has_key( namespace_id ):
|
|
del self.namespace_id_to_hash[ namespace_id ]
|
|
|
|
if self.import_addresses.has_key( namespace_id ):
|
|
del self.import_addresses[ namespace_id ]
|
|
|
|
return
|
|
|
|
|
|
def commit_preorder( self, nameop, current_block_number ):
|
|
"""
|
|
Record that a name was preordered.
|
|
"""
|
|
|
|
name_hash = nameop['preorder_name_hash']
|
|
commit_nameop = self.sanitize_op( nameop )
|
|
commit_nameop['block_number'] = current_block_number
|
|
commit_nameop['history'] = {}
|
|
commit_nameop['op'] = NAME_PREORDER
|
|
self.preorders[ name_hash ] = commit_nameop
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.preorders[name_hash] )
|
|
return nameop
|
|
|
|
|
|
def commit_registration( self, nameop, current_block_number ):
|
|
"""
|
|
Record a name registration to have taken place at a particular
|
|
block number.
|
|
This removes the preorder for the name as well, and updates
|
|
expiration and hash-to-name indexes.
|
|
"""
|
|
|
|
name = nameop['name']
|
|
sender = nameop['sender']
|
|
address = nameop['address']
|
|
|
|
# NOTE: on renewals, these fields should match the above two fields.
|
|
recipient = nameop['recipient']
|
|
recipient_address = nameop['recipient_address']
|
|
|
|
# is this a renewal?
|
|
if self.is_name_registered( name ):
|
|
|
|
self.commit_renewal( nameop, current_block_number )
|
|
|
|
else:
|
|
|
|
# registered!
|
|
preorder = self.commit_remove_preorder( name, sender, recipient_address )
|
|
del preorder['history']
|
|
|
|
# preorder becomes a history snapshot
|
|
preorder = self.sanitize_op( preorder )
|
|
preorder['history_snapshot'] = True
|
|
|
|
prior_history = {}
|
|
prior_block_number = preorder['block_number']
|
|
|
|
# if we're replacing an expired name, merge this preorder into the name's history
|
|
if self.name_records.get( name ) is not None:
|
|
name_rec = self.name_records[name]
|
|
prior_history = name_rec['history']
|
|
prior_block_number = name_rec['block_number']
|
|
|
|
# name changed
|
|
preorder['name'] = name
|
|
|
|
# preserve name historical information at the point just before the preorder
|
|
prior_history[ preorder['block_number'] ] = [{
|
|
"name": name_rec['name'],
|
|
"value_hash": name_rec['value_hash'],
|
|
"sender": name_rec['sender'],
|
|
"sender_pubkey": name_rec.get('sender_pubkey', None),
|
|
"address": name_rec['address'],
|
|
"block_number": name_rec['block_number'],
|
|
"preorder_block_number": name_rec['preorder_block_number'],
|
|
"revoked": name_rec['revoked'],
|
|
"op": name_rec['op'],
|
|
"opcode": name_rec['opcode'],
|
|
"txid": name_rec['txid'],
|
|
"vtxindex": int(name_rec['vtxindex']),
|
|
"op_fee": name_rec['op_fee'],
|
|
"importer": name_rec['importer'],
|
|
"importer_address": name_rec['importer_address'],
|
|
"history_snapshot": True,
|
|
"first_registered": name_rec['first_registered'],
|
|
"last_renewed": name_rec['last_renewed']
|
|
}]
|
|
|
|
for optional_field in ['consensus_hash', 'transfer_send_block_id']:
|
|
if optional_field in name_rec.keys():
|
|
prior_history[ preorder['block_number'] ][0][optional_field] = name_rec[optional_field]
|
|
|
|
name_record = {
|
|
'name': name,
|
|
'value_hash': None, # i.e. the hex hash of profile data in immutable storage.
|
|
'sender': str(recipient), # the recipient is the expected future sender
|
|
'sender_pubkey': nameop.get('sender_pubkey', None),
|
|
'address': str(recipient_address),
|
|
|
|
'block_number': prior_block_number,
|
|
'preorder_block_number': preorder['block_number'],
|
|
'first_registered': current_block_number,
|
|
'last_renewed': current_block_number,
|
|
'revoked': False,
|
|
|
|
'op': NAME_REGISTRATION,
|
|
'txid': str(nameop['txid']),
|
|
'vtxindex': int(nameop['vtxindex']),
|
|
'opcode': str(nameop['opcode']),
|
|
'op_fee': int(preorder['op_fee']),
|
|
|
|
# (not imported)
|
|
'importer': None,
|
|
'importer_address': None,
|
|
|
|
'history': {
|
|
# history of this name so far: its preorder
|
|
current_block_number: [preorder]
|
|
}
|
|
}
|
|
|
|
# merge prior history...
|
|
name_record['history'].update( prior_history )
|
|
|
|
ns_id = get_namespace_from_name( name )
|
|
namespace = self.namespaces.get( ns_id, None )
|
|
if namespace is None:
|
|
raise Exception("Name with no namespace: '%s'" % name)
|
|
|
|
expires = current_block_number + namespace['lifetime']
|
|
|
|
self.name_records[ name ] = name_record
|
|
|
|
# update secondary indexes
|
|
self.owner_names[ recipient ].append( str(name) )
|
|
self.address_names[ recipient_address ].append( str(name) )
|
|
self.hash_names[ hash256_trunc128( name ) ] = name
|
|
|
|
if not self.block_name_expires.has_key( expires ):
|
|
self.block_name_expires[ expires ] = [name]
|
|
else:
|
|
self.block_name_expires[ expires ].append( name )
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.name_records[ name ] )
|
|
return nameop
|
|
|
|
|
|
def commit_renewal( self, nameop, current_block_number ):
|
|
"""
|
|
Commit a name renewal. Push back its expiration.
|
|
"""
|
|
|
|
name = nameop['name']
|
|
txid = nameop['txid']
|
|
op = "%s:" % NAME_REGISTRATION
|
|
block_last_renewed = self.name_records[name]['last_renewed']
|
|
|
|
ns_id = get_namespace_from_name( name )
|
|
namespace = self.namespaces.get( ns_id, None )
|
|
if namespace is None:
|
|
raise Exception("Name with no namespace: '%s'" % name)
|
|
|
|
old_expires = block_last_renewed + namespace['lifetime']
|
|
expires = current_block_number + namespace['lifetime']
|
|
|
|
# name no longer expires at the current expiry time
|
|
if self.block_name_expires.has_key( old_expires ):
|
|
if name in self.block_name_expires[ old_expires ]:
|
|
self.block_name_expires[ old_expires ].remove( name )
|
|
|
|
if not self.block_name_expires.has_key( expires ):
|
|
self.block_name_expires[ expires ] = [name]
|
|
else:
|
|
self.block_name_expires[ expires ].append( name )
|
|
|
|
# save diff
|
|
self.save_name_diff( name, current_block_number, ['last_renewed', 'txid', 'vtxindex', 'op', 'opcode', 'consensus_hash', 'transfer_send_block_id'] )
|
|
|
|
# apply diff
|
|
self.name_records[name]['last_renewed'] = current_block_number
|
|
self.name_records[name]['txid'] = txid
|
|
self.name_records[name]['vtxindex'] = nameop['vtxindex']
|
|
self.name_records[name]['op'] = op
|
|
self.name_records[name]['opcode'] = nameop['opcode']
|
|
if self.name_records.has_key('consensus_hash'):
|
|
del self.name_records['consensus_hash']
|
|
|
|
# propagate information back to virtualchian for snapshotting
|
|
nameop.update( self.name_records[name] )
|
|
return nameop
|
|
|
|
|
|
def commit_update( self, nameop, current_block_number ):
|
|
"""
|
|
Commit an update to a name's profile data.
|
|
NOTE: nameop['name'] will have been defined by log_update.
|
|
"""
|
|
|
|
name_consensus_hash = nameop['name_hash']
|
|
txid = nameop['txid']
|
|
|
|
try:
|
|
name = nameop['name']
|
|
except:
|
|
log.error( "No 'name' in nameop: %s" % nameop )
|
|
name = self.name_consensus_hash_name[ name_consensus_hash ]
|
|
del self.name_consensus_hash_name[ name_consensus_hash ]
|
|
|
|
# save diff
|
|
self.save_name_diff( name, current_block_number, ['value_hash', 'txid', 'vtxindex', 'opcode', 'op', 'consensus_hash', 'transfer_send_block_id'] )
|
|
|
|
# apply diff
|
|
self.name_records[name]['value_hash'] = nameop['update_hash']
|
|
self.name_records[name]['txid'] = txid
|
|
self.name_records[name]['vtxindex'] = nameop['vtxindex']
|
|
self.name_records[name]['opcode'] = nameop['opcode']
|
|
self.name_records[name]['op'] = NAME_UPDATE
|
|
|
|
# NOTE: obtained from log_update
|
|
self.name_records[name]['consensus_hash'] = nameop['consensus_hash']
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.name_records[name] )
|
|
return nameop
|
|
|
|
|
|
def commit_transfer( self, nameop, current_block_number ):
|
|
"""
|
|
Commit a transfer--update the name record to indicate the recipient of the
|
|
transaction as the new owner.
|
|
"""
|
|
|
|
name = nameop['name']
|
|
sender = nameop['sender']
|
|
address = nameop.get('address', None)
|
|
recipient = nameop['recipient']
|
|
recipient_address = nameop['recipient_address']
|
|
keep_data = nameop['keep_data']
|
|
txid = nameop['txid']
|
|
opcode = nameop['opcode']
|
|
|
|
transfer_send_block_id = self.lookup_block_id_from_consensus_hash( nameop['consensus_hash'] )
|
|
if transfer_send_block_id is None:
|
|
log.error("FATAL: no block for consensus hash '%s'" % nameop['consensus_hash'])
|
|
sys.exit(1)
|
|
|
|
op = TRANSFER_KEEP_DATA
|
|
if not keep_data:
|
|
op = TRANSFER_REMOVE_DATA
|
|
|
|
log.debug("Name '%s': %s >%s %s" % (name, sender, op, recipient))
|
|
|
|
# save diff
|
|
changed = ['sender', 'address', 'txid', 'vtxindex', 'opcode', 'op', 'sender_pubkey', 'consensus_hash', 'transfer_send_block_id']
|
|
if not keep_data:
|
|
changed.append( 'value_hash' )
|
|
|
|
self.save_name_diff( name, current_block_number, changed )
|
|
|
|
# apply diff
|
|
self.name_records[name]['sender'] = recipient
|
|
self.name_records[name]['sender_pubkey'] = None
|
|
self.name_records[name]['address'] = recipient_address
|
|
self.name_records[name]['txid'] = txid
|
|
self.name_records[name]['vtxindex'] = nameop['vtxindex']
|
|
self.name_records[name]['opcode'] = opcode
|
|
self.name_records[name]['op'] = "%s%s" % (NAME_TRANSFER, op)
|
|
self.name_records[name]['transfer_send_block_id'] = transfer_send_block_id # not directly covered by consensus hash, but indirectly
|
|
|
|
if not keep_data:
|
|
self.name_records[name]['value_hash'] = None
|
|
|
|
# update secondary indexes
|
|
if self.owner_names.has_key(sender) and name in self.owner_names[sender]:
|
|
self.owner_names[sender].remove( name )
|
|
if len(self.owner_names[sender]) == 0:
|
|
del self.owner_names[sender]
|
|
|
|
if self.address_names.has_key(address) and name in self.address_names[address]:
|
|
self.address_names[address].remove( name )
|
|
if len(self.address_names[address]) == 0:
|
|
del self.address_names[address]
|
|
|
|
self.owner_names[recipient].append( name )
|
|
self.address_names[recipient_address].append( name )
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.name_records[name] )
|
|
return nameop
|
|
|
|
|
|
def commit_revoke( self, nameop, current_block_number ):
|
|
"""
|
|
Commit a revocation--blow away the name
|
|
"""
|
|
|
|
name = nameop['name']
|
|
txid = nameop['txid']
|
|
sender = nameop['sender']
|
|
address = nameop.get('address', None)
|
|
opcode = nameop['opcode']
|
|
op = NAME_REVOKE
|
|
|
|
# save diff
|
|
self.save_name_diff( name, current_block_number, ['revoked', 'txid', 'vtxindex', 'opcode', 'op', 'value_hash', 'consensus_hash', 'transfer_send_block_id'] )
|
|
|
|
# apply diff
|
|
self.name_records[name]['revoked'] = True
|
|
self.name_records[name]['txid'] = txid
|
|
self.name_records[name]['vtxindex'] = nameop['vtxindex']
|
|
self.name_records[name]['opcode'] = opcode
|
|
self.name_records[name]['op'] = op
|
|
self.name_records[name]['value_hash'] = None
|
|
|
|
# update secondary indexes
|
|
if self.owner_names.has_key( sender ) and name in self.owner_names[sender]:
|
|
self.owner_names[sender].remove( name )
|
|
if len(self.owner_names[sender]) == 0:
|
|
del self.owner_names[sender]
|
|
|
|
if self.address_names.has_key( address ) and name in self.address_names[address]:
|
|
self.address_names[address].remove( name )
|
|
if len(self.address_names[address]) == 0:
|
|
del self.address_names[address]
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.name_records[name] )
|
|
return nameop
|
|
|
|
|
|
def commit_name_import( self, nameop, current_block_number ):
|
|
"""
|
|
Commit a name import--register it and set the owner and update hash.
|
|
"""
|
|
|
|
name = str(nameop['name'])
|
|
recipient = str(nameop['recipient'])
|
|
recipient_address = str(nameop['recipient_address'])
|
|
importer = nameop['sender']
|
|
if type(importer) == list:
|
|
importer = importer[0]
|
|
|
|
importer = str(importer)
|
|
importer_address = str(nameop['address'])
|
|
update_hash = str(nameop['update_hash'])
|
|
|
|
name_without_namespace = get_name_from_fq_name( name )
|
|
namespace_id = get_namespace_from_name( name )
|
|
namespace = self.get_namespace_reveal( namespace_id )
|
|
if namespace is None:
|
|
raise Exception("Name without revealed namespace: '%s'" % name )
|
|
|
|
old_recipient = None
|
|
old_recipient_address = None
|
|
|
|
if self.name_records.has_key( name ):
|
|
|
|
name_rec_fields = [
|
|
'value_hash',
|
|
'sender',
|
|
'sender_pubkey',
|
|
'address',
|
|
'txid',
|
|
'vtxindex',
|
|
'importer',
|
|
'importer_address',
|
|
'consensus_hash'
|
|
]
|
|
|
|
# save diff
|
|
self.save_name_diff( name, current_block_number, name_rec_fields )
|
|
|
|
old_recipient = self.name_records[name]['sender']
|
|
old_recipient_address = self.name_records[name]['address']
|
|
|
|
# apply diff
|
|
self.name_records[name]['value_hash'] = update_hash
|
|
self.name_records[name]['sender'] = recipient # expected future sender
|
|
self.name_records[name]['sender_pubkey'] = str(nameop['sender_pubkey']) # NOTE: this is the *importer's* public key
|
|
self.name_records[name]['address'] = recipient_address
|
|
self.name_records[name]['txid'] = str(nameop['txid'])
|
|
self.name_records[name]['vtxindex'] = nameop['vtxindex']
|
|
self.name_records[name]['importer'] = importer
|
|
self.name_records[name]['importer_address'] = str(nameop['address'])
|
|
|
|
else:
|
|
|
|
# nameop becomes a history snapshot
|
|
nameop = self.sanitize_op( nameop )
|
|
nameop['history_snapshot'] = True
|
|
|
|
# fix up nameop to be the first history item
|
|
nameop['value_hash'] = nameop['update_hash']
|
|
del nameop['update_hash']
|
|
|
|
nameop['importer'] = importer
|
|
nameop['importer_address'] = importer_address
|
|
|
|
name_record = {
|
|
# snapshotted data
|
|
'name': name,
|
|
'value_hash': update_hash,
|
|
'sender': recipient, # recipient is expected future sender
|
|
'sender_pubkey': str(nameop['sender_pubkey']), # NOTE: this is the *importer's* public key
|
|
'address': recipient_address,
|
|
|
|
'block_number': current_block_number,
|
|
'preorder_block_number': current_block_number, # NOTE: an import is considered to be an atomic preorder/register
|
|
'first_registered': current_block_number,
|
|
'last_renewed': current_block_number,
|
|
'revoked': False,
|
|
|
|
'op': NAME_IMPORT,
|
|
'txid': str(nameop['txid']),
|
|
'vtxindex': str(nameop['vtxindex']),
|
|
'op_fee': price_name( name_without_namespace, namespace ),
|
|
|
|
'importer': importer,
|
|
'importer_address': importer_address,
|
|
|
|
# ancillary data
|
|
'history': {
|
|
|
|
# history for this nameop starts at this block number
|
|
current_block_number: [copy.deepcopy( nameop )]
|
|
},
|
|
|
|
'opcode': str(nameop['opcode'])
|
|
}
|
|
|
|
self.name_records[ name ] = name_record
|
|
|
|
# update secondary indexes...
|
|
self.owner_names[ recipient ].append( str(name) )
|
|
if old_recipient is not None:
|
|
if self.owner_names.has_key( old_recipient ) and name in self.owner_names[old_recipient]:
|
|
self.owner_names[ old_recipient ].remove( name )
|
|
if len(self.owner_names[old_recipient]) == 0:
|
|
del self.owner_names[old_recipient]
|
|
|
|
self.address_names[ recipient_address ].append( str(name) )
|
|
if old_recipient_address is not None:
|
|
if self.address_names.has_key( old_recipient_address ) and name in self.address_names[old_recipient_address]:
|
|
self.address_names[ old_recipient_address ].remove( name )
|
|
if len(self.address_names[old_recipient_address]) == 0:
|
|
del self.address_names[old_recipient_address]
|
|
|
|
self.hash_names[ hash256_trunc128( name ) ] = name
|
|
|
|
expires = current_block_number + namespace['lifetime']
|
|
if not self.block_name_expires.has_key( expires ):
|
|
self.block_name_expires[ expires ] = [name]
|
|
else:
|
|
self.block_name_expires[ expires ].append( name )
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.name_records[name] )
|
|
return nameop
|
|
|
|
|
|
def commit_namespace_preorder( self, nameop, block_number ):
|
|
"""
|
|
Commit a NAMESPACE_PREORER, so we can subsequently accept
|
|
a namespace reveal from the sender.
|
|
|
|
The namespace will be preordered, but not yet defined
|
|
(i.e. we know it exists, but we don't know its parameters).
|
|
"""
|
|
|
|
namespace_id_hash = nameop['namespace_id_hash']
|
|
sender = nameop['sender']
|
|
nameop['history'] = {}
|
|
nameop['block_number'] = block_number
|
|
nameop['op'] = NAMESPACE_PREORDER
|
|
|
|
# this namespace is preordered, but not yet defined
|
|
# store non-virtualchian fields
|
|
self.namespace_preorders[ namespace_id_hash ] = self.sanitize_op( nameop )
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
return nameop
|
|
|
|
|
|
def commit_namespace_reveal( self, nameop, block_number ):
|
|
"""
|
|
Commit a NAMESPACE_REVEAL nameop, so we can subsequently accept
|
|
a sequence of preordered names from the sender.
|
|
|
|
The namespace will be revealed, but not yet ready
|
|
(i.e. we know its parameters, but only the sender can operate on its names)
|
|
|
|
The namespace will be owned by the recipient, not the sender.
|
|
"""
|
|
|
|
# collision?
|
|
if nameop.get('collision'):
|
|
return False
|
|
|
|
namespace_id = nameop['namespace_id']
|
|
namespace_id_hash = nameop['namespace_id_hash']
|
|
|
|
self.namespace_reveals[ namespace_id ] = self.sanitize_op( nameop )
|
|
self.namespace_id_to_hash[ nameop['namespace_id'] ] = namespace_id_hash
|
|
|
|
if namespace_id_hash in self.namespace_preorders:
|
|
# preserve the old namespace preorder
|
|
old_preorder = self.namespace_preorders[namespace_id_hash]
|
|
del old_preorder['history']
|
|
|
|
# old preorder becomes a history snapshot
|
|
old_preorder = self.sanitize_op( old_preorder )
|
|
old_preorder['history_snapshot'] = True
|
|
|
|
self.namespace_reveals[ namespace_id ]['history'] = {
|
|
block_number: [old_preorder]
|
|
}
|
|
|
|
self.namespace_reveals[ namespace_id ]['block_number'] = old_preorder['block_number']
|
|
self.namespace_reveals[ namespace_id ]['reveal_block'] = block_number
|
|
self.namespace_reveals[ namespace_id ]['op'] = NAMESPACE_REVEAL
|
|
|
|
del self.namespace_preorders[namespace_id_hash]
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.namespace_reveals[ namespace_id ] )
|
|
return nameop
|
|
|
|
else:
|
|
log.error("BUG: no namespace preorder for '%s' (hash '%s')" % (namespace_id, namespace_id_hash))
|
|
raise Exception("BUG: no namespace preorder for '%s' (hash '%s')" % (namespace_id, namespace_id_hash))
|
|
|
|
|
|
def commit_namespace_ready( self, nameop, block_number ):
|
|
"""
|
|
Mark a namespace as ready for external name registrations.
|
|
The given nameop is a NAMESPACE_READY nameop.
|
|
"""
|
|
|
|
namespace_id = nameop['namespace_id']
|
|
sender = nameop['sender']
|
|
|
|
namespace = self.namespace_reveals[ namespace_id ]
|
|
|
|
# save to history (duplicate it)
|
|
namespace_reveal = copy.deepcopy( namespace )
|
|
history = namespace_reveal['history']
|
|
del namespace_reveal['history']
|
|
|
|
# namespace reveal becomes history snapshot
|
|
namespace_reveal = self.sanitize_op( namespace_reveal )
|
|
namespace_reveal['history_snapshot'] = True
|
|
|
|
if history.has_key( block_number ):
|
|
history[block_number].append( namespace_reveal )
|
|
else:
|
|
history[block_number] = [namespace_reveal]
|
|
|
|
namespace['history'] = history
|
|
namespace['opcode'] = nameop['opcode']
|
|
namespace['op'] = NAMESPACE_READY
|
|
namespace['ready_block'] = block_number
|
|
namespace['sender'] = sender
|
|
namespace['txid'] = nameop['txid']
|
|
namespace['vtxindex'] = nameop['vtxindex']
|
|
|
|
self.commit_remove_namespace_import( namespace_id, namespace['namespace_id_hash'] )
|
|
|
|
# namespace is ready!
|
|
self.namespaces[ namespace_id ] = namespace
|
|
|
|
# propagate information back to virtualchain for snapshotting
|
|
nameop.update( self.namespaces[ namespace_id ] )
|
|
return nameop
|
|
|
|
|
|
def log_prescan_find_collisions( self, pending_ops, all_nameops, block_id ):
|
|
"""
|
|
Do a pass over all operations in this block to find colliding operations:
|
|
* check all 'NAME_REGISTRATION' operations, and if
|
|
two or more for a given name are valid, then mark all
|
|
the NAME_REGISTRATIONs for that name as invalid.
|
|
* check all 'NAMESPACE_REVEAL' operations, and if
|
|
two or more for a given namespace are valid, then mark
|
|
all NAMESPACE_REVEALs for that namespace as invald.
|
|
|
|
Return ([list of colliding names], [list of colliding namespace_ids])
|
|
"""
|
|
|
|
if not self.prescanned:
|
|
|
|
valid_registrations = {} # map name to indexes in all_nameops
|
|
valid_namespaces = {}
|
|
|
|
# find all name collisions
|
|
for i in xrange(0, len(all_nameops)):
|
|
nameop = all_nameops[i]
|
|
if nameop['opcode'] != 'NAME_REGISTRATION':
|
|
continue
|
|
|
|
valid = self.log_registration( pending_ops, nameop, block_id )
|
|
if valid:
|
|
if nameop['name'] in valid_registrations.keys():
|
|
|
|
# mark all as collided
|
|
valid_registrations[nameop['name']].append( i )
|
|
|
|
else:
|
|
|
|
# valid, but not yet collided
|
|
valid_registrations[nameop['name']] = [i]
|
|
|
|
|
|
# find all namespace collisions
|
|
for i in xrange(0, len(all_nameops)):
|
|
nameop = all_nameops[i]
|
|
if nameop['opcode'] != 'NAMESPACE_REVEAL':
|
|
continue
|
|
|
|
valid = self.log_namespace_reveal( pending_ops, nameop, block_id )
|
|
if valid:
|
|
if nameop['namespace_id'] in valid_namespaces.keys():
|
|
|
|
# mark all as collided
|
|
valid_namespaces[nameop['namespace_id']].append( i )
|
|
|
|
else:
|
|
|
|
# valid, not yet collided
|
|
valid_namespaces[nameop['namespace_id']] = [i]
|
|
|
|
colliding_names = []
|
|
colliding_namespaces = []
|
|
|
|
for name, namelist in valid_registrations.items():
|
|
if len(namelist) > 1:
|
|
# all collided
|
|
for i in namelist:
|
|
colliding_names.append( all_nameops[i]['name'] )
|
|
|
|
|
|
for namespace_id, namespacelist in valid_namespaces.items():
|
|
if len(namespacelist) > 1:
|
|
# all collided
|
|
for i in namespacelist:
|
|
colliding_namespaces.append( all_nameops[i]['namespace_id'] )
|
|
|
|
|
|
self.prescanned = True
|
|
self.colliding_names = colliding_names
|
|
self.colliding_namespaces = colliding_namespaces
|
|
|
|
return (self.colliding_names, self.colliding_namespaces)
|
|
|
|
|
|
def log_prescan_reset( self ):
|
|
"""
|
|
Reset the db for scanning.
|
|
"""
|
|
self.prescanned = False
|
|
self.colliding_names = None
|
|
self.colliding_namespaces = None
|
|
|
|
|
|
def log_announce( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log an announcement from the blockstack developers.
|
|
Return (True, blockchain_id) if it is well-formed, and came from one of the blockchain IDs
|
|
listed in the config file.
|
|
Return (False, None) otherwise.
|
|
"""
|
|
|
|
sender = nameop['sender']
|
|
sending_blockchain_id = nameop['sender']
|
|
found = False
|
|
|
|
for blockchain_id in self.announce_ids:
|
|
blockchain_namerec = self.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, None)
|
|
|
|
return (True, str(sending_blockchain_id))
|
|
|
|
|
|
def log_preorder( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log a preorder of a name at a particular block number.
|
|
|
|
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.
|
|
"""
|
|
|
|
preorder_name_hash = nameop['preorder_name_hash']
|
|
consensus_hash = nameop['consensus_hash']
|
|
sender = nameop['sender']
|
|
|
|
# must be unique in this block
|
|
for pending_preorders in pending_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 self.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 self.is_consensus_hash_valid( block_id, consensus_hash ):
|
|
log.debug("Invalid consensus hash '%s'" % consensus_hash )
|
|
return False
|
|
|
|
# sender must be beneath quota
|
|
if len( self.owner_names.get( sender, [] ) ) >= 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 log_registration( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Progess 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.
|
|
|
|
NAME_REGISTRATION is not allowed during a namespace import, so the namespace must be ready.
|
|
|
|
NOTE: it is *imperative* that this method does *not* modify nameop.
|
|
|
|
Return True if accepted.
|
|
Return False if not.
|
|
"""
|
|
|
|
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 p2pkh given")
|
|
return False
|
|
|
|
name_fee = None
|
|
namespace = None
|
|
|
|
# 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 self.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 self.is_namespace_ready( namespace_id ):
|
|
log.debug("Namespace '%s' is not ready" % namespace_id)
|
|
return False
|
|
|
|
# get namespace...
|
|
namespace = self.get_namespace( namespace_id )
|
|
|
|
# preordered?
|
|
name_preorder = self.get_name_preorder( name, sender, register_addr )
|
|
if name_preorder is not None:
|
|
|
|
# name must be preordered by the same sender
|
|
if name_preorder['sender'] != sender:
|
|
log.debug("Name '%s' was not preordered by %s" % (name, sender))
|
|
return False
|
|
|
|
# name can't be registered if it was reordered before its namespace was ready
|
|
if not namespace.has_key('ready_block') or name_preorder['block_number'] < namespace['ready_block']:
|
|
log.debug("Name '%s' preordered before namespace '%s' was ready" % (name, namespace_id))
|
|
return False
|
|
|
|
# fee was included in the preorder
|
|
if not 'op_fee' in name_preorder:
|
|
log.debug("Name '%s' preorder did not pay the fee" % (name))
|
|
return False
|
|
|
|
name_fee = name_preorder['op_fee']
|
|
|
|
|
|
elif self.is_name_registered( name ):
|
|
|
|
# name must be owned by the recipient already
|
|
if not self.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 self.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
|
|
|
|
name_fee = nameop['op_fee']
|
|
|
|
else:
|
|
|
|
# does not exist and not preordered
|
|
log.debug("Name '%s' does not exist, or is not preordered by %s" % (name, sender))
|
|
return False
|
|
|
|
# cannot exceed quota
|
|
if len( self.owner_names.get( recipient, [] ) ) >= MAX_NAMES_PER_SENDER:
|
|
log.debug("Recipient '%s' has exceeded quota" % recipient)
|
|
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
|
|
|
|
# regster/renewal
|
|
return True
|
|
|
|
|
|
def log_update(self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log 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_hash']
|
|
sender = nameop['sender']
|
|
|
|
# deny updates if we exceed quota--the only legal operations are to revoke or transfer.
|
|
names = self.owner_names.get( sender, [] )
|
|
if len(names) > MAX_NAMES_PER_SENDER:
|
|
log.debug("Sender '%s' has exceeded quota: only transfers or revokes are allowed" % (sender))
|
|
return False
|
|
|
|
name, consensus_hash = self.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 )
|
|
|
|
if self.name_records.get(name, None) is None:
|
|
log.debug("Name '%s' does not exist" % name)
|
|
return False
|
|
|
|
# namespace must be ready
|
|
if not self.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 self.is_name_revoked( name ):
|
|
log.debug("Name '%s' is revoked" % name)
|
|
return False
|
|
|
|
# name must not be expired
|
|
if self.is_name_expired( name, self.lastblock ):
|
|
log.debug("Name '%s' is expired" % name)
|
|
return False
|
|
|
|
# the name must be registered
|
|
if not self.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 self.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...
|
|
self.name_consensus_hash_name[ name_consensus_hash ] = name
|
|
nameop['name'] = name
|
|
nameop['consensus_hash'] = consensus_hash
|
|
|
|
return True
|
|
|
|
|
|
def log_transfer( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log 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_hash']
|
|
name = self.hash_names.get( name_hash )
|
|
|
|
consensus_hash = nameop['consensus_hash']
|
|
sender = nameop['sender']
|
|
recipient = nameop['recipient']
|
|
|
|
if name is None:
|
|
# invalid
|
|
log.debug("No name found for '%s'" % name_hash )
|
|
return False
|
|
|
|
namespace_id = get_namespace_from_name( name )
|
|
|
|
if self.name_records.get( name, None ) is None:
|
|
log.debug("Name '%s' does not exist" % name)
|
|
return False
|
|
|
|
# namespace must be ready
|
|
if not self.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 self.is_name_revoked( name ):
|
|
log.debug("Name '%s' is revoked" % name)
|
|
return False
|
|
|
|
# name must not be expired
|
|
if self.is_name_expired( name, self.lastblock ):
|
|
log.debug("Name '%s' is expired" % name)
|
|
return False
|
|
|
|
if not self.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 self.is_name_registered( name ):
|
|
# name is not registered
|
|
log.debug("Name '%s' is not registered" % name)
|
|
return False
|
|
|
|
if not self.is_name_owner( name, sender ):
|
|
# sender doesn't own the name
|
|
log.debug("Name '%s' is not owned by %s (but %s)" % (name, sender, self.get_name_owner(name)))
|
|
return False
|
|
|
|
if recipient in self.owner_names.keys():
|
|
|
|
# recipient already has names...
|
|
recipient_names = self.owner_names[ recipient ]
|
|
if name in recipient_names:
|
|
# recipient already owns the name
|
|
log.debug("Recipient %s already owns '%s'" % (recipient, name))
|
|
return False
|
|
|
|
if len(recipient_names) >= MAX_NAMES_PER_SENDER:
|
|
# transfer would exceed quota
|
|
log.debug("Recipient %s has exceeded name quota" % recipient)
|
|
return False
|
|
|
|
# remember the name, so we don't have to look it up later
|
|
nameop['name'] = name
|
|
return True
|
|
|
|
|
|
def log_revoke( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
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_TRANSFER 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
|
|
if self.name_records.get( name, None ) is None:
|
|
log.debug("Name '%s' does not exist" % name)
|
|
return False
|
|
|
|
# namespace must be ready
|
|
if not self.is_namespace_ready( namespace_id ):
|
|
log.debug("Namespace '%s' is not ready" % namespace_id )
|
|
return False
|
|
|
|
# name must not be revoked
|
|
if self.is_name_revoked( name ):
|
|
log.debug("Name '%s' is revoked" % name)
|
|
return False
|
|
|
|
# name must not be expired
|
|
if self.is_name_expired( name, self.lastblock ):
|
|
log.debug("Name '%s' is expired" % name)
|
|
return False
|
|
|
|
# the name must be registered
|
|
if not self.is_name_registered( name ):
|
|
log.debug("Name '%s' is not registered" % name )
|
|
return False
|
|
|
|
# the sender must own this name
|
|
if not self.is_name_owner( name, sender ):
|
|
log.debug("Name '%s' is not owned by %s" % (name, sender))
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def log_name_import( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
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
|
|
|
|
Return True if accepted
|
|
Return False if not
|
|
"""
|
|
|
|
name = str(nameop['name'])
|
|
sender = str(nameop['sender'])
|
|
sender_pubkey = 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_b40( name ) or "+" in name or name.count(".") > 1:
|
|
log.debug("Malformed name '%s': non-base-38 characters" % name)
|
|
return False
|
|
|
|
namespace_id = get_namespace_from_name( name )
|
|
|
|
# namespace must be revealed, but not ready
|
|
if not self.is_namespace_revealed( namespace_id ):
|
|
log.debug("Namespace '%s' is not revealed" % namespace_id )
|
|
return False
|
|
|
|
namespace = self.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 = pybitcoin.BitcoinPublicKey( str(sender_pubkey_hex) )
|
|
sender_address = sender_pubkey.address()
|
|
|
|
import_addresses = self.import_addresses.get(namespace_id, None)
|
|
|
|
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( sender_pubkey_hex )
|
|
self.import_addresses[namespace_id] = import_addresses
|
|
|
|
# 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
|
|
if self.is_name_registered( name ):
|
|
log.warning("Overwriting already-imported name '%s'" % name)
|
|
|
|
# good!
|
|
return True
|
|
|
|
|
|
def log_namespace_preorder( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
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['namespace_id_hash']
|
|
consensus_hash = nameop['consensus_hash']
|
|
|
|
# namespace must not exist
|
|
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 self.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 self.is_consensus_hash_valid( block_id, consensus_hash ):
|
|
|
|
valid_consensus_hashes = self.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 log_namespace_reveal( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log 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.
|
|
|
|
It is *imperative* that this method not modify nameop
|
|
|
|
Return True if accepted
|
|
Return False if not
|
|
"""
|
|
|
|
namespace_id = nameop['namespace_id']
|
|
namespace_id_hash = nameop['namespace_id_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 p2kh 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 self.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 self.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 = self.get_namespace_preorder( namespace_id_hash )
|
|
if namespace_preorder is None:
|
|
# not preordered
|
|
log.debug("Namespace '%s' is not preordered" % namespace_id )
|
|
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
|
|
|
|
# can begin import
|
|
return True
|
|
|
|
|
|
def log_namespace_ready( self, pending_nameops, nameop, block_id ):
|
|
"""
|
|
Log a NAMESPACE_READY operation to the name database.
|
|
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 self.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 = self.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 self.is_namespace_ready( namespace_id ):
|
|
# namespace already exists
|
|
log.debug("Namespace '%s' is already registered" % namespace_id )
|
|
return False
|
|
|
|
# can commit imported nameops
|
|
return True
|
|
|
|
|
|
def get_namespace_from_name( name ):
|
|
"""
|
|
Get a fully-qualified name's namespace, if it has one.
|
|
It's the sequence of characters after the last "." in the name.
|
|
If there is no "." in the name, then it belongs to the null
|
|
namespace (i.e. the empty string will be returned)
|
|
"""
|
|
if "." not in name:
|
|
# empty namespace
|
|
return ""
|
|
|
|
return name.split(".")[-1]
|
|
|
|
|
|
def get_name_from_fq_name( name ):
|
|
"""
|
|
Given a fully-qualified name, get the name part.
|
|
It's the sequence of characters before the last "." in the name.
|
|
|
|
Return None if malformed
|
|
"""
|
|
if "." not in name:
|
|
# malformed
|
|
return None
|
|
|
|
return name.split(".")[0]
|
|
|
|
|
|
def price_name( name, namespace ):
|
|
"""
|
|
Calculate the price of a name (without its namespace ID), given the
|
|
namespace parameters.
|
|
|
|
The minimum price is 1 satoshi
|
|
"""
|
|
|
|
base = namespace['base']
|
|
coeff = namespace['coeff']
|
|
buckets = namespace['buckets']
|
|
|
|
bucket_exponent = 0
|
|
discount = 1.0
|
|
|
|
if len(name) < len(buckets):
|
|
bucket_exponent = buckets[len(name)-1]
|
|
else:
|
|
bucket_exponent = buckets[-1]
|
|
|
|
# no vowel discount?
|
|
if sum( [name.lower().count(v) for v in ["a", "e", "i", "o", "u", "y"]] ) == 0:
|
|
# no vowels!
|
|
discount = max( discount, namespace['no_vowel_discount'] )
|
|
|
|
# non-alpha discount?
|
|
if sum( [name.lower().count(v) for v in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_"]] ) > 0:
|
|
# non-alpha!
|
|
discount = max( discount, namespace['nonalpha_discount'] )
|
|
|
|
price = (float(coeff * (base ** bucket_exponent)) / float(discount)) * NAME_COST_UNIT
|
|
if price < NAME_COST_UNIT:
|
|
price = NAME_COST_UNIT
|
|
|
|
return price
|
|
|
|
|
|
def price_namespace( namespace_id ):
|
|
"""
|
|
Calculate the cost of a namespace.
|
|
"""
|
|
|
|
testset = default_blockstack_opts( virtualchain.get_config_filename() )['testset']
|
|
|
|
if len(namespace_id) == 1:
|
|
if testset:
|
|
return TESTSET_NAMESPACE_1_CHAR_COST
|
|
else:
|
|
return NAMESPACE_1_CHAR_COST
|
|
|
|
elif len(namespace_id) in [2, 3]:
|
|
if testset:
|
|
return TESTSET_NAMESPACE_23_CHAR_COST
|
|
else:
|
|
return NAMESPACE_23_CHAR_COST
|
|
|
|
elif len(namespace_id) in [4, 5, 6, 7]:
|
|
if testset:
|
|
return TESTSET_NAMESPACE_4567_CHAR_COST
|
|
else:
|
|
return NAMESPACE_4567_CHAR_COST
|
|
|
|
else:
|
|
if testset:
|
|
return TESTSET_NAMESPACE_8UP_CHAR_COST
|
|
else:
|
|
return NAMESPACE_8UP_CHAR_COST
|
|
|