mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-10 08:50:49 +08:00
2483 lines
88 KiB
Python
2483 lines
88 KiB
Python
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack
|
|
~~~~~
|
|
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2016-2018 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 sqlite3
|
|
import subprocess
|
|
import json
|
|
import simplejson
|
|
import traceback
|
|
import os
|
|
import sys
|
|
import copy
|
|
import time
|
|
import random
|
|
|
|
# hack around absolute paths
|
|
curr_dir = os.path.abspath( os.path.join( os.path.dirname(__file__), ".." ) )
|
|
sys.path.insert( 0, curr_dir )
|
|
|
|
from ..config import *
|
|
from ..operations import *
|
|
from ..hashing import *
|
|
from ..scripts import *
|
|
from ..b40 import *
|
|
from ..util import db_query_execute, db_format_query
|
|
|
|
import virtualchain
|
|
|
|
log = virtualchain.get_logger("blockstack-server")
|
|
|
|
BLOCKSTACK_DB_SCRIPT = ""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
-- NOTE: history_id is a fully-qualified name or namespace ID.
|
|
-- NOTE: creator_address is the address that owned the name or namespace ID at the time of insertion
|
|
-- NOTE: value_hash is the associated value hash for this history entry at the time of insertion.
|
|
-- NOTE: history_data is a JSON blob with the operation that was committed at this point in time.
|
|
CREATE TABLE history( txid TEXT NOT NULL,
|
|
history_id STRING,
|
|
creator_address STRING,
|
|
block_id INT NOT NULL,
|
|
vtxindex INT NOT NULL,
|
|
op TEXT NOT NULL,
|
|
opcode TEXT NOT NULL,
|
|
value_hash TEXT,
|
|
history_data TEXT NOT NULL,
|
|
PRIMARY KEY(txid,block_id,vtxindex) );
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
-- CREATE INDEX history_block_id_index ON history( history_id, block_id );
|
|
CREATE INDEX history_id_index ON history( history_id );
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
-- NOTE: this table only grows.
|
|
-- The only time rows can be taken out is when a name or
|
|
-- namespace successfully matches it.
|
|
CREATE TABLE preorders( preorder_hash TEXT NOT NULL,
|
|
consensus_hash TEXT NOT NULL,
|
|
sender TEXT NOT NULL,
|
|
sender_pubkey TEXT,
|
|
address TEXT,
|
|
block_number INT NOT NULL,
|
|
op TEXT NOT NULL,
|
|
op_fee INT NOT NULL,
|
|
txid TEXT NOT NULL,
|
|
vtxindex INT,
|
|
burn_address TEXT NOT NULL,
|
|
|
|
-- primary key includes the block number and txid, so an expired preorder can be overwritten
|
|
PRIMARY KEY(preorder_hash,block_number,txid));
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
-- NOTE: this table includes revealed namespaces
|
|
-- NOTE: 'buckets' is a string representation of an array of 16 integers.
|
|
CREATE TABLE namespaces( namespace_id STRING NOT NULL,
|
|
preorder_hash TEXT NOT NULL,
|
|
version INT,
|
|
sender TEXT NOT NULL,
|
|
sender_pubkey TEXT,
|
|
address TEXT,
|
|
recipient TEXT NOT NULL,
|
|
recipient_address TEXT,
|
|
block_number INT NOT NULL,
|
|
reveal_block INT NOT NULL,
|
|
op TEXT NOT NULL,
|
|
op_fee INT NOT NULL,
|
|
txid TEXT NOT NULL NOT NULL,
|
|
vtxindex INT NOT NULL,
|
|
lifetime INT NOT NULL,
|
|
coeff INT NOT NULL,
|
|
base INT NOT NULL,
|
|
buckets TEXT NOT NULL,
|
|
nonalpha_discount INT NOT NULL,
|
|
no_vowel_discount INT NOT NULL,
|
|
ready_block INT NOT NULL,
|
|
|
|
-- primary key includes block number, so an expired revealed namespace can be re-revealed
|
|
PRIMARY KEY(namespace_id,block_number)
|
|
);
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
CREATE TABLE name_records( name STRING NOT NULL,
|
|
preorder_hash TEXT NOT NULL,
|
|
name_hash128 TEXT NOT NULL,
|
|
namespace_id STRING NOT NULL,
|
|
namespace_block_number INT NOT NULL,
|
|
value_hash TEXT,
|
|
sender TEXT NOT NULL,
|
|
sender_pubkey TEXT,
|
|
address TEXT,
|
|
block_number INT NOT NULL,
|
|
preorder_block_number INT NOT NULL,
|
|
first_registered INT NOT NULL,
|
|
last_renewed INT NOT NULL,
|
|
revoked INT NOT NULL,
|
|
op TEXT NOT NULL,
|
|
txid TEXT NOT NULL,
|
|
vtxindex INT NOT NULL,
|
|
op_fee INT NOT NULL,
|
|
importer TEXT,
|
|
importer_address TEXT,
|
|
consensus_hash TEXT,
|
|
|
|
-- for compatibility with previous versions' quirks
|
|
last_creation_op STRING NOT NULL,
|
|
|
|
-- primary key includes block number, so an expired name can be re-registered
|
|
PRIMARY KEY(name,block_number),
|
|
|
|
-- namespace must exist
|
|
FOREIGN KEY(namespace_id,namespace_block_number) REFERENCES namespaces(namespace_id,block_number)
|
|
);
|
|
"""
|
|
|
|
'''
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
CREATE TABLE ops_hashes( block_id INTEGER PRIMARY KEY NOT NULL,
|
|
ops_hash STRING NOT NULL );
|
|
"""
|
|
'''
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
CREATE INDEX hash_names_index ON name_records( name_hash128, name );
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
CREATE INDEX value_hash_names_index on name_records( value_hash, name );
|
|
"""
|
|
|
|
BLOCKSTACK_DB_SCRIPT += """
|
|
-- turn on foreign key constraints
|
|
PRAGMA foreign_keys = ON;
|
|
"""
|
|
|
|
|
|
def namedb_create( path ):
|
|
"""
|
|
Create a sqlite3 db at the given path.
|
|
Create all the tables and indexes we need.
|
|
"""
|
|
|
|
global BLOCKSTACK_DB_SCRIPT
|
|
|
|
if os.path.exists( path ):
|
|
raise Exception("Database '%s' already exists" % path)
|
|
|
|
lines = [l + ";" for l in BLOCKSTACK_DB_SCRIPT.split(";")]
|
|
con = sqlite3.connect( path, isolation_level=None, timeout=2**30 )
|
|
|
|
for line in lines:
|
|
db_query_execute(con, line, ())
|
|
|
|
con.row_factory = namedb_row_factory
|
|
|
|
# add user-defined functions
|
|
con.create_function("namespace_lifetime_multiplier", 2, namedb_get_namespace_lifetime_multiplier)
|
|
con.create_function("namespace_lifetime_grace_period", 2, namedb_get_namespace_lifetime_grace_period)
|
|
return con
|
|
|
|
|
|
def namedb_open( path ):
|
|
"""
|
|
Open a connection to our database
|
|
"""
|
|
con = sqlite3.connect( path, isolation_level=None, timeout=2**30 )
|
|
con.row_factory = namedb_row_factory
|
|
|
|
# add user-defined functions
|
|
con.create_function("namespace_lifetime_multiplier", 2, namedb_get_namespace_lifetime_multiplier)
|
|
con.create_function("namespace_lifetime_grace_period", 2, namedb_get_namespace_lifetime_grace_period)
|
|
return con
|
|
|
|
|
|
def namedb_row_factory( cursor, row ):
|
|
"""
|
|
Row factor to enforce some additional types:
|
|
* force 'revoked' to be a bool
|
|
"""
|
|
d = {}
|
|
for idx, col in enumerate( cursor.description ):
|
|
if col[0] == 'revoked':
|
|
if row[idx] == 0:
|
|
d[col[0]] = False
|
|
elif row[idx] == 1:
|
|
d[col[0]] = True
|
|
elif row[idx] is None:
|
|
d[col[0]] = None
|
|
else:
|
|
raise Exception("Invalid value for 'revoked': %s" % row[idx])
|
|
|
|
else:
|
|
d[col[0]] = row[idx]
|
|
|
|
return d
|
|
|
|
|
|
def namedb_get_namespace_lifetime_multiplier( block_height, namespace_id ):
|
|
"""
|
|
User-defined sqlite3 function that gets the namespace
|
|
lifetime multiplier at a particular block height.
|
|
"""
|
|
try:
|
|
namespace_lifetime_multiplier = get_epoch_namespace_lifetime_multiplier( block_height, namespace_id )
|
|
return namespace_lifetime_multiplier
|
|
except Exception, e:
|
|
try:
|
|
with open("/tmp/blockstack_db_exception.txt", "w") as f:
|
|
f.write(traceback.format_exc())
|
|
except:
|
|
raise
|
|
|
|
raise
|
|
|
|
|
|
def namedb_get_namespace_lifetime_grace_period( block_height, namespace_id ):
|
|
"""
|
|
User-defined sqlite3 function that gets the namespace
|
|
lifetime grace period at a particular block height.
|
|
"""
|
|
try:
|
|
namespace_lifetime_grace_period = get_epoch_namespace_lifetime_grace_period( block_height, namespace_id )
|
|
return namespace_lifetime_grace_period
|
|
except Exception, e:
|
|
try:
|
|
with open("/tmp/blockstack_db_exception.txt", "w") as f:
|
|
f.write(traceback.format_exc())
|
|
except:
|
|
raise
|
|
|
|
raise
|
|
|
|
|
|
def namedb_find_missing_and_extra(cur, record, table_name):
|
|
"""
|
|
Find the set of fields missing from record, and set of extra fields from record, based on the db schema.
|
|
Return (missing, extra)
|
|
"""
|
|
rec_missing = []
|
|
rec_extra = []
|
|
|
|
# sanity check: all fields must be defined
|
|
name_fields_rows = db_query_execute(cur, 'PRAGMA table_info({})'.format(table_name), ())
|
|
name_fields = []
|
|
for row in name_fields_rows:
|
|
name_fields.append( row['name'] )
|
|
|
|
# make sure each column has a record field
|
|
for f in name_fields:
|
|
if f not in record.keys():
|
|
rec_missing.append( f )
|
|
|
|
# make sure each record field has a column
|
|
for k in record.keys():
|
|
if k not in name_fields:
|
|
rec_extra.append( k )
|
|
|
|
return rec_missing, rec_extra
|
|
|
|
|
|
def namedb_assert_fields_match( cur, record, table_name, record_matches_columns=True, columns_match_record=True ):
|
|
"""
|
|
Ensure that the fields of a given record match
|
|
the columns of the given table.
|
|
* if record_match_columns, then the keys in record must match all columns.
|
|
* if columns_match_record, then the columns must match the keys in the record.
|
|
|
|
Return True if so.
|
|
Raise an exception if not.
|
|
"""
|
|
rec_missing, rec_extra = namedb_find_missing_and_extra(cur, record, table_name)
|
|
if (len(rec_missing) > 0 and columns_match_record) or (len(rec_extra) > 0 and record_matches_columns):
|
|
raise Exception("Invalid record: missing = %s, extra = %s" %
|
|
(",".join(rec_missing), ",".join(rec_extra)))
|
|
|
|
return True
|
|
|
|
|
|
def namedb_insert_prepare( cur, record, table_name ):
|
|
"""
|
|
Prepare to insert a record, but make sure
|
|
that all of the column names have values first!
|
|
|
|
Return an INSERT INTO statement on success.
|
|
Raise an exception if not.
|
|
"""
|
|
|
|
namedb_assert_fields_match( cur, record, table_name )
|
|
|
|
columns = record.keys()
|
|
columns.sort()
|
|
|
|
values = []
|
|
for c in columns:
|
|
if record[c] == False:
|
|
values.append(0)
|
|
elif record[c] == True:
|
|
values.append(1)
|
|
else:
|
|
values.append(record[c])
|
|
|
|
values = tuple(values)
|
|
|
|
field_placeholders = ",".join( ["?"] * len(columns) )
|
|
|
|
query = "INSERT INTO %s (%s) VALUES (%s);" % (table_name, ",".join(columns), field_placeholders)
|
|
log.debug(namedb_format_query(query, values))
|
|
|
|
return (query, values)
|
|
|
|
|
|
def namedb_update_prepare( cur, primary_key_or_keys, input_record, table_name, must_equal=[], only_if={} ):
|
|
"""
|
|
Prepare to update a record, but make sure that the fields in input_record
|
|
correspond to acual columns.
|
|
Also, enforce any fields that must be equal to the fields
|
|
in the given record (must_equal), and require that certian fields in record
|
|
have certain values first (only_if)
|
|
|
|
Return an UPDATE ... SET ... WHERE statement on success.
|
|
Raise an exception if not.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY
|
|
"""
|
|
|
|
record = copy.deepcopy( input_record )
|
|
must_equal_dict = dict( [(c, None) for c in must_equal] )
|
|
|
|
# extract primary key
|
|
# sanity check: primary key cannot be mutated
|
|
primary_keys = []
|
|
if isinstance(primary_key_or_keys, (str,unicode)):
|
|
primary_keys = [primary_key_or_keys]
|
|
else:
|
|
primary_keys = primary_key_or_keys
|
|
|
|
for primary_key_col in primary_keys:
|
|
primary_key_value = record.get(primary_key_col, None)
|
|
assert primary_key_value is not None, "BUG: no primary key value given in record"
|
|
assert primary_key_col in must_equal, "BUG: primary key set to change"
|
|
|
|
assert len(must_equal) > 0, "BUG: no identifying information for this record"
|
|
|
|
# find set of columns that will change
|
|
update_columns = []
|
|
for k in input_record.keys():
|
|
if k not in must_equal:
|
|
update_columns.append( k )
|
|
|
|
# record keys correspond to real columns
|
|
namedb_assert_fields_match( cur, record, table_name, columns_match_record=False )
|
|
|
|
# must_equal keys correspond to real columns
|
|
namedb_assert_fields_match( cur, must_equal_dict, table_name, columns_match_record=False )
|
|
|
|
# only_if keys correspond to real columns
|
|
namedb_assert_fields_match( cur, only_if, table_name, columns_match_record=False )
|
|
|
|
# only_if does not overlap with must_equal
|
|
assert len( set(must_equal).intersection(only_if.keys()) ) == 0, "BUG: only_if and must_equal overlap"
|
|
|
|
update_values = []
|
|
for c in update_columns:
|
|
if record[c] == False:
|
|
update_values.append(0)
|
|
elif record[c] == True:
|
|
update_values.append(1)
|
|
else:
|
|
update_values.append(record[c])
|
|
|
|
update_values = tuple(update_values)
|
|
update_set = [("%s = ?" % c) for c in update_columns]
|
|
|
|
where_set = []
|
|
where_values = []
|
|
for c in must_equal:
|
|
if record[c] is None:
|
|
where_set.append( "%s IS NULL" % c )
|
|
elif record[c] == True:
|
|
where_set.append( "%s = 1" % c )
|
|
elif record[c] == False:
|
|
where_set.append( "%s = 0" % c )
|
|
else:
|
|
where_set.append( "%s = ?" % c)
|
|
where_values.append( record[c] )
|
|
|
|
|
|
for c in only_if.keys():
|
|
if only_if[c] is None:
|
|
where_set.append( "%s IS NULL" % c)
|
|
elif record[c] == True:
|
|
where_set.append( "%s = 1" % c )
|
|
elif record[c] == False:
|
|
where_set.append( "%s = 0" % c )
|
|
else:
|
|
where_set.append( "%s = ?" % c)
|
|
where_values.append( only_if[c] )
|
|
|
|
where_values = tuple(where_values)
|
|
|
|
query = "UPDATE %s SET %s WHERE %s" % (table_name, ", ".join(update_set), " AND ".join(where_set))
|
|
|
|
log.debug(namedb_format_query(query, update_values + where_values))
|
|
|
|
return (query, update_values + where_values)
|
|
|
|
|
|
def namedb_update_must_equal( rec, change_fields ):
|
|
"""
|
|
Generate the set of fields that must stay the same across an update.
|
|
"""
|
|
|
|
must_equal = []
|
|
if len(change_fields) != 0:
|
|
given = rec.keys()
|
|
for k in given:
|
|
if k not in change_fields:
|
|
must_equal.append(k)
|
|
|
|
return must_equal
|
|
|
|
|
|
def namedb_delete_prepare( cur, primary_key, primary_key_value, table_name ):
|
|
"""
|
|
Prepare to delete a record, but make sure the fields in record
|
|
correspond to actual columns.
|
|
|
|
Return a DELETE FROM ... WHERE statement on success.
|
|
Raise an Exception if not.
|
|
|
|
DO NOT CALL THIS METHOD DIRETLY
|
|
"""
|
|
# primary key corresponds to a real column
|
|
namedb_assert_fields_match( cur, {primary_key: primary_key_value}, table_name, columns_match_record=False )
|
|
|
|
query = "DELETE FROM %s WHERE %s = ?;" % (table_name, primary_key)
|
|
values = (primary_key_value,)
|
|
return (query, values)
|
|
|
|
|
|
def namedb_format_query( query, values ):
|
|
"""
|
|
Turn a query into a string for printing.
|
|
Useful for debugging.
|
|
"""
|
|
return db_format_query(query, values)
|
|
|
|
|
|
def namedb_query_execute( cur, query, values ):
|
|
"""
|
|
Execute a query. If it fails, abort. Retry with timeouts on lock
|
|
|
|
DO NOT CALL THIS DIRECTLY.
|
|
"""
|
|
return db_query_execute(cur, query, values)
|
|
|
|
|
|
def namedb_preorder_insert( cur, preorder_rec ):
|
|
"""
|
|
Add a name or namespace preorder record, if it doesn't exist already.
|
|
|
|
DO NOT CALL THIS DIRECTLY.
|
|
"""
|
|
|
|
preorder_row = copy.deepcopy( preorder_rec )
|
|
|
|
assert 'preorder_hash' in preorder_row, "BUG: missing preorder_hash"
|
|
|
|
try:
|
|
preorder_query, preorder_values = namedb_insert_prepare( cur, preorder_row, "preorders" )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: Failed to insert name preorder '%s'" % preorder_row['preorder_hash'])
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, preorder_query, preorder_values )
|
|
return True
|
|
|
|
|
|
def namedb_preorder_remove( cur, preorder_hash ):
|
|
"""
|
|
Remove a preorder hash.
|
|
|
|
DO NOT CALL THIS DIRECTLY.
|
|
"""
|
|
try:
|
|
query, values = namedb_delete_prepare( cur, 'preorder_hash', preorder_hash, 'preorders' )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: Failed to delete preorder with hash '%s'" % preorder_hash )
|
|
os.abort()
|
|
|
|
log.debug(namedb_format_query(query, values))
|
|
namedb_query_execute( cur, query, values )
|
|
return True
|
|
|
|
|
|
def namedb_name_fields_check( name_rec ):
|
|
"""
|
|
Make sure that a name record has some fields
|
|
that must always be present:
|
|
* name
|
|
* namespace_id
|
|
* name_hash128
|
|
|
|
Makes the record suitable for insertion/update.
|
|
NOTE: MODIFIES name_rec
|
|
"""
|
|
|
|
if not name_rec.has_key('name'):
|
|
raise Exception("BUG: name record has no name")
|
|
|
|
# extract namespace ID if it's not already there
|
|
if not name_rec.has_key('namespace_id'):
|
|
name_rec['namespace_id'] = get_namespace_from_name( name_rec['name'] )
|
|
|
|
# extract name_hash if it's not already there
|
|
if not name_rec.has_key('name_hash128'):
|
|
name_rec['name_hash128'] = hash256_trunc128( name_rec['name'] )
|
|
|
|
return True
|
|
|
|
|
|
def namedb_name_insert( cur, input_name_rec ):
|
|
"""
|
|
Add the given name record to the database,
|
|
if it doesn't exist already.
|
|
"""
|
|
|
|
name_rec = copy.deepcopy( input_name_rec )
|
|
namedb_name_fields_check( name_rec )
|
|
|
|
try:
|
|
query, values = namedb_insert_prepare( cur, name_rec, "name_records" )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: Failed to insert name '%s'" % name_rec['name'])
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, query, values )
|
|
|
|
return True
|
|
|
|
|
|
def namedb_name_update( cur, opcode, input_opdata, only_if={}, constraints_ignored=[] ):
|
|
"""
|
|
Update an existing name in the database.
|
|
If non-empty, only update the given fields.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
opdata = copy.deepcopy( input_opdata )
|
|
namedb_name_fields_check( opdata )
|
|
mutate_fields = op_get_mutate_fields( opcode )
|
|
|
|
if opcode not in OPCODE_CREATION_OPS:
|
|
assert 'name' not in mutate_fields, "BUG: 'name' listed as a mutate field for '%s'" % (opcode)
|
|
|
|
# reduce opdata down to the given fields....
|
|
must_equal = namedb_update_must_equal( opdata, mutate_fields )
|
|
must_equal += ['name','block_number']
|
|
|
|
for ignored in constraints_ignored:
|
|
if ignored in must_equal:
|
|
# ignore this constraint
|
|
must_equal.remove( ignored )
|
|
|
|
try:
|
|
query, values = namedb_update_prepare( cur, ['name', 'block_number'], opdata, "name_records", must_equal=must_equal, only_if=only_if )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to update name '%s'" % opdata['name'])
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, query, values )
|
|
|
|
try:
|
|
assert cur.rowcount == 1, "Updated %s row(s)" % cur.rowcount
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to update name '%s'" % opdata['name'])
|
|
log.error("Query: %s", "".join( ["%s %s" % (frag, "'%s'" % val if type(val) in [str, unicode] else val) for (frag, val) in zip(query.split("?"), values + ("",))] ))
|
|
os.abort()
|
|
|
|
return True
|
|
|
|
|
|
def namedb_namespace_fields_check( namespace_rec ):
|
|
"""
|
|
Given a namespace record, make sure the following fields are present:
|
|
* namespace_id
|
|
* buckets
|
|
|
|
Makes the record suitable for insertion/update.
|
|
NOTE: MODIFIES namespace_rec
|
|
"""
|
|
|
|
assert namespace_rec.has_key('namespace_id'), "BUG: namespace record has no ID"
|
|
assert namespace_rec.has_key('buckets'), 'BUG: missing price buckets'
|
|
assert isinstance(namespace_rec['buckets'], str), 'BUG: namespace data is not in canonical form'
|
|
|
|
return namespace_rec
|
|
|
|
|
|
def namedb_namespace_insert( cur, input_namespace_rec ):
|
|
"""
|
|
Add a namespace to the database,
|
|
if it doesn't exist already.
|
|
It must be a *revealed* namespace, not a ready namespace
|
|
(to mark a namespace as ready, you should use the namedb_apply_operation()
|
|
method).
|
|
"""
|
|
|
|
namespace_rec = copy.deepcopy( input_namespace_rec )
|
|
|
|
namedb_namespace_fields_check( namespace_rec )
|
|
|
|
try:
|
|
query, values = namedb_insert_prepare( cur, namespace_rec, "namespaces" )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: Failed to insert revealed namespace '%s'" % namespace_rec['namespace_id'])
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, query, values )
|
|
return True
|
|
|
|
|
|
def namedb_namespace_update( cur, opcode, input_opdata, only_if={}, constraints_ignored=[] ):
|
|
"""
|
|
Make a namespace ready.
|
|
Only works if the namespace is *not* ready.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
opdata = copy.deepcopy( input_opdata )
|
|
assert opdata.has_key('namespace_id'), "BUG: namespace record has no ID"
|
|
|
|
mutate_fields = op_get_mutate_fields( opcode )
|
|
|
|
if opcode not in OPCODE_CREATION_OPS:
|
|
assert 'namespace_id' not in mutate_fields, "BUG: 'namespace_id' listed as a mutate field for '%s'" % (opcode)
|
|
|
|
else:
|
|
namedb_namespace_fields_check( opdata )
|
|
|
|
# reduce opdata down to the given fields....
|
|
must_equal = namedb_update_must_equal( opdata, mutate_fields )
|
|
must_equal += ['namespace_id','block_number']
|
|
|
|
for ignored in constraints_ignored:
|
|
if ignored in must_equal:
|
|
# ignore this constraint
|
|
must_equal.remove( ignored )
|
|
|
|
try:
|
|
query, values = namedb_update_prepare( cur, ['namespace_id', 'block_number'], opdata, "namespaces", must_equal=must_equal, only_if={} )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to update name '%s'" % opdata['namespace_id'])
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, query, values )
|
|
|
|
try:
|
|
assert cur.rowcount == 1, "Updated %s row(s)" % cur.rowcount
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to update name '%s'" % opdata['namespace_id'])
|
|
log.error("Query: %s", "".join( ["%s %s" % (frag, "'%s'" % val if type(val) in [str, unicode] else val) for (frag, val) in zip(query.split("?"), values + ("",))] ))
|
|
os.abort()
|
|
|
|
return True
|
|
|
|
|
|
def namedb_op_sanity_check( opcode, op_data, record ):
|
|
"""
|
|
Sanity checks over operation and state graph data:
|
|
* opcode and op_data must be consistent
|
|
* record must have an opcode
|
|
* the given opcode must be reachable from it.
|
|
"""
|
|
|
|
assert 'address' in record, "BUG: current record has no 'address' field"
|
|
|
|
assert op_data.has_key('op'), "BUG: operation data is missing its 'op'"
|
|
op_data_opcode = op_get_opcode_name( op_data['op'] )
|
|
|
|
assert record.has_key('op'), "BUG: current record is missing its 'op'"
|
|
cur_opcode = op_get_opcode_name( record['op'] )
|
|
|
|
assert op_data_opcode is not None, "BUG: undefined operation '%s'" % op_data['op']
|
|
assert cur_opcode is not None, "BUG: undefined current operation '%s'" % record['op']
|
|
|
|
if op_data_opcode != opcode:
|
|
# only allowed of the serialized opcode is the same
|
|
# (i.e. as is the case for register/renew)
|
|
assert NAME_OPCODES.get( op_data_opcode, None ) is not None, "BUG: unrecognized opcode '%s'" % op_data_opcode
|
|
assert NAME_OPCODES.get( opcode, None ) is not None, "BUG: unrecognized opcode '%s'" % opcode
|
|
|
|
assert NAME_OPCODES[op_data_opcode] == NAME_OPCODES[opcode], "BUG: %s != %s" % (opcode, op_data_opcode)
|
|
|
|
assert opcode in OPCODE_SEQUENCE_GRAPH, "BUG: impossible to arrive at operation '%s'" % opcode
|
|
assert cur_opcode in OPCODE_SEQUENCE_GRAPH, "BUG: impossible to have processed operation '%s'" % cur_opcode
|
|
assert opcode in OPCODE_SEQUENCE_GRAPH[ cur_opcode ], "BUG: impossible sequence from '%s' to '%s'" % (cur_opcode, opcode)
|
|
|
|
return True
|
|
|
|
|
|
def namedb_state_mutation_sanity_check( opcode, op_data ):
|
|
"""
|
|
Make sure all mutate fields for this operation are present.
|
|
Return True if so
|
|
Raise exception if not
|
|
"""
|
|
|
|
# sanity check: each mutate field in the operation must be defined in op_data, even if it's null.
|
|
missing = []
|
|
mutate_fields = op_get_mutate_fields( opcode )
|
|
for field in mutate_fields:
|
|
if field not in op_data.keys():
|
|
missing.append( field )
|
|
|
|
assert len(missing) == 0, ("BUG: operation '%s' is missing the following fields: %s" % (opcode, ",".join(missing)))
|
|
return True
|
|
|
|
|
|
def namedb_state_transition_sanity_check( opcode, op_data, history_id, cur_record, record_table ):
|
|
"""
|
|
Sanity checks: make sure that:
|
|
* the opcode and op_data are consistent with one another.
|
|
* the history_id, cur_record, and record_table are consistent with one another.
|
|
|
|
Return True if so.
|
|
Raise an exception if not.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
namedb_op_sanity_check( opcode, op_data, cur_record )
|
|
|
|
if opcode in OPCODE_NAME_STATE_TRANSITIONS:
|
|
# name state transition
|
|
assert record_table == "name_records", "BUG: name state transition opcode (%s) on table %s" % (opcode, record_table)
|
|
assert cur_record.has_key('name'), "BUG: name state transition with no name"
|
|
assert op_data.has_key('name'), "BUG: name state transition with no name"
|
|
assert op_data['name'] == history_id, 'BUG: name op data is for the wrong name ({} != {})'.format(op_data['name'], history_id)
|
|
assert op_data['name'] == cur_record['name'], 'BUG: name op data is for the wrong name ({} != {})'.format(op_data['name'], cur_record['name'])
|
|
assert cur_record['name'] == history_id, "BUG: history ID '%s' != '%s'" % (history_id, cur_record['name'])
|
|
|
|
elif opcode in OPCODE_NAMESPACE_STATE_TRANSITIONS:
|
|
# namespace state transition
|
|
assert record_table == "namespaces", "BUG: namespace state transition opcode (%s) on table %s" % (opcode, record_table)
|
|
assert cur_record.has_key('namespace_id'), "BUG: namespace state transition with no namespace ID"
|
|
assert cur_record['namespace_id'] == history_id, "BUG: history ID '%s' != '%s'" % (history_id, cur_record['namespace_id'])
|
|
assert op_data['namespace_id'] == history_id, 'BUG: name op data is for the wrong name ({} != {})'.format(op_data['namespace_id'], history_id)
|
|
assert op_data['namespace_id'] == cur_record['namespace_id'], 'BUG: name op data is for the wrong name ({} != {})'.format(op_data['namespace_id'], cur_record['namespace_id'])
|
|
assert cur_record['namespace_id'] == history_id, "BUG: history ID '%s' != '%s'" % (history_id, cur_record['namespace_id'])
|
|
|
|
assert cur_record.has_key('block_number'), 'BUG: name state transition with no block number'
|
|
if op_data.has_key('block_number'):
|
|
assert op_data['block_number'] == cur_record['block_number'], 'BUG: block number mismatch ({} != {})'.format(op_data['block_number'], cur_record['block_number'])
|
|
|
|
return True
|
|
|
|
|
|
def namedb_state_transition( cur, opcode, op_data, block_id, vtxindex, txid, history_id, cur_record, record_table, constraints_ignored=[] ):
|
|
"""
|
|
Given an operation (opcode, op_data), a point in time (block_id, vtxindex, txid), and a current
|
|
record (history_id, cur_record), apply the operation to the record and save the delta to the record's
|
|
history. Also, insert or update the new record into the db.
|
|
|
|
The cur_record must exist already.
|
|
|
|
Return the newly updated record on success, with all compatibility quirks preserved.
|
|
Raise an exception on failure.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
# sanity check: must be a state-transitioning operation
|
|
try:
|
|
assert opcode in OPCODE_NAME_STATE_TRANSITIONS + OPCODE_NAMESPACE_STATE_TRANSITIONS, "BUG: opcode '%s' is not a state-transition"
|
|
assert 'opcode' not in op_data, 'BUG: opcode not allowed in op_data'
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("BUG: opcode '%s' is not a state-transition operation" % opcode)
|
|
os.abort()
|
|
|
|
# make sure we have a name/namespace_id and block number
|
|
op_data_name = copy.deepcopy(op_data)
|
|
|
|
if opcode in OPCODE_NAME_STATE_TRANSITIONS:
|
|
# name state transition
|
|
op_data_name['name'] = history_id
|
|
|
|
elif opcode in OPCODE_NAMESPACE_STATE_TRANSITIONS:
|
|
# namespace state transition
|
|
op_data_name['namespace_id'] = history_id
|
|
|
|
# sanity check make sure we got valid state transition data
|
|
try:
|
|
assert cur_record.has_key('block_number'), 'current record does not have a block number'
|
|
op_data_name['block_number'] = cur_record['block_number']
|
|
|
|
rc = namedb_state_transition_sanity_check( opcode, op_data_name, history_id, cur_record, record_table )
|
|
if not rc:
|
|
raise Exception("State transition sanity checks failed")
|
|
|
|
rc = namedb_state_mutation_sanity_check( opcode, op_data_name )
|
|
if not rc:
|
|
raise Exception("State mutation sanity checks failed")
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: state transition sanity checks failed")
|
|
os.abort()
|
|
|
|
# 1. generate the new record that will be used for consensus.
|
|
# It will be the new data overlayed on the current record, with all quirks applied.
|
|
new_record = {}
|
|
new_record.update(cur_record)
|
|
new_record.update(op_data_name)
|
|
new_record['opcode'] = opcode
|
|
|
|
canonicalized_record = op_canonicalize_quirks(opcode, new_record, cur_record)
|
|
canonicalized_record['opcode'] = opcode
|
|
|
|
rc = namedb_history_save(cur, opcode, history_id, None, new_record.get('value_hash', None), block_id, vtxindex, txid, canonicalized_record)
|
|
if not rc:
|
|
log.error("FATAL: failed to save history for '%s' at (%s, %s)" % (history_id, block_id, vtxindex))
|
|
os.abort()
|
|
|
|
rc = False
|
|
merged_new_record = None
|
|
|
|
# 2. Store the actual op_data, to be returned on name lookups
|
|
# Don't store extra fields that don't belong in the db (i.e. that we don't have colunms for), but preserve them across the write.
|
|
stored_op_data = {}
|
|
stored_op_data.update(op_data_name)
|
|
|
|
# separate out the extras
|
|
_, op_data_extra = namedb_find_missing_and_extra(cur, stored_op_data, record_table)
|
|
if len(op_data_extra) > 0:
|
|
log.debug("Remove extra fields: {}".format(','.join(op_data_extra)))
|
|
for extra in op_data_extra:
|
|
del stored_op_data[extra]
|
|
|
|
if opcode in OPCODE_NAME_STATE_TRANSITIONS:
|
|
# name state transition
|
|
rc = namedb_name_update( cur, opcode, stored_op_data, constraints_ignored=constraints_ignored )
|
|
if not rc:
|
|
log.error("FATAL: opcode is not a state-transition operation on names")
|
|
os.abort()
|
|
|
|
merged_new_record = namedb_get_name(cur, history_id, block_id, include_history=False, include_expired=True)
|
|
|
|
elif opcode in OPCODE_NAMESPACE_STATE_TRANSITIONS:
|
|
# namespace state transition
|
|
rc = namedb_namespace_update( cur, opcode, stored_op_data, constraints_ignored=constraints_ignored )
|
|
if not rc:
|
|
log.error("FATAL: opcode is not a state-transition operation on namespaces")
|
|
os.abort()
|
|
|
|
merged_new_record = namedb_get_namespace(cur, history_id, block_id, include_history=False, include_expired=True)
|
|
|
|
# 3. success! make sure the merged_new_record is consistent with canonicalized_record
|
|
for f in merged_new_record:
|
|
if f not in canonicalized_record:
|
|
raise Exception("canonicalized record is missing {}".format(f))
|
|
|
|
return canonicalized_record
|
|
|
|
|
|
def namedb_state_create_sanity_check( opcode, op_data, history_id, preorder_record, record_table ):
|
|
"""
|
|
Sanity checks on a preorder and a state-creation operation:
|
|
* the opcode must match the op_data
|
|
* the history_id and operation must match the preorder
|
|
* everything must match the record table.
|
|
|
|
Return True on success
|
|
Raise an exception on error.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
namedb_op_sanity_check( opcode, op_data, preorder_record )
|
|
preorder_opcode = op_get_opcode_name(preorder_record['op'])
|
|
|
|
if opcode in OPCODE_NAME_STATE_CREATIONS:
|
|
# name state transition
|
|
assert record_table == "name_records", "BUG: name state transition opcode (%s) on table %s" % (opcode, record_table)
|
|
assert preorder_opcode in OPCODE_NAME_STATE_PREORDER, "BUG: preorder record opcode '%s' is not a name preorder" % (preorder_opcode)
|
|
|
|
assert 'name' in op_data, 'BUG: no name in op_data'
|
|
assert 'block_number' in op_data, 'BUG: no block_number in op_data'
|
|
|
|
elif opcode in OPCODE_NAMESPACE_STATE_CREATIONS:
|
|
# namespace state transition
|
|
assert record_table == "namespaces", "BUG: namespace state transition opcode (%s) on table %s" % (opcode, record_table)
|
|
assert preorder_opcode in OPCODE_NAMESPACE_STATE_PREORDER, "BUG: preorder record opcode '%s' is not a namespace preorder" % (preorder_opcode)
|
|
|
|
assert 'namespace_id' in op_data, 'BUG: no namespace_id in op_data'
|
|
assert 'block_number' in op_data, 'BUG: no block_number in op_data'
|
|
|
|
return True
|
|
|
|
|
|
def namedb_state_create( cur, opcode, new_record, block_id, vtxindex, txid, history_id, preorder_record, record_table, constraints_ignored=[] ):
|
|
"""
|
|
Given an operation and a new record (opcode, new_record), a point in time (block_id, vtxindex, txid), and a preorder
|
|
record for a known record (history_id, preorder_record), create the initial name or namespace using
|
|
the preorder and operation's data. Record the preorder as history.
|
|
|
|
This operation will allow the caller to update an existing name or namespace if it is being re-registered.
|
|
It is up to the caller to verify that the name or namespace does not exist at the time of this call.
|
|
|
|
Returns the data to snapshot on success (with all compatibility quirks preserved)
|
|
Raise an exception on failure.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
# sanity check: must be a state-creation operation
|
|
if opcode not in OPCODE_NAME_STATE_CREATIONS + OPCODE_NAMESPACE_STATE_CREATIONS or opcode in OPCODE_NAME_STATE_IMPORTS:
|
|
log.error("FATAL: Opcode '%s' is not a state-creating operation" % opcode)
|
|
os.abort()
|
|
|
|
# did this name or namespace previously exist?
|
|
exists = False
|
|
prev_rec = None
|
|
if opcode in OPCODE_NAMESPACE_STATE_CREATIONS:
|
|
prev_rec = namedb_get_namespace(cur, history_id, block_id, include_expired=True, include_history=False)
|
|
if prev_rec is not None:
|
|
exists = True
|
|
|
|
elif opcode in OPCODE_NAME_STATE_CREATIONS or opcode in OPCODE_NAME_STATE_IMPORTS:
|
|
prev_rec = namedb_get_name(cur, history_id, block_id, include_expired=True, include_history=False)
|
|
if prev_rec is not None:
|
|
exists = True
|
|
|
|
try:
|
|
assert 'op' in preorder_record.keys(), 'BUG: no preorder op'
|
|
assert 'preorder_hash' in preorder_record.keys(), "BUG: no preorder hash"
|
|
assert 'block_number' in preorder_record.keys(), "BUG: preorder has no block number"
|
|
assert 'vtxindex' in preorder_record.keys(), "BUG: preorder has no vtxindex"
|
|
assert 'txid' in preorder_record.keys(), "BUG: preorder has no txid"
|
|
assert 'burn_address' in preorder_record.keys(), 'BUG: preorder has no burn address'
|
|
|
|
if prev_rec is not None:
|
|
# block_number cannot change
|
|
assert prev_rec['block_number'] == new_record['block_number'], 'BUG: trying to change block number from {} to {} for "{}"'.format(prev_rec['block_number'], new_record['block_number'], history_id)
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: no preorder hash")
|
|
os.abort()
|
|
|
|
try:
|
|
if not exists:
|
|
# sanity check to make sure we got valid state-creation data
|
|
rc = namedb_state_create_sanity_check( opcode, new_record, history_id, preorder_record, record_table )
|
|
if not rc:
|
|
raise Exception("state-creation sanity check on '%s' failed" % opcode )
|
|
|
|
rc = namedb_state_mutation_sanity_check( opcode, new_record )
|
|
if not rc:
|
|
raise Exception("State mutation sanity checks failed")
|
|
|
|
assert 'opcode' not in new_record, 'BUG: opcode not allowed in op_data'
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: state-creation sanity check failed")
|
|
os.abort()
|
|
|
|
# save the preorder as history.
|
|
rc = namedb_history_save(cur, preorder_record['opcode'], history_id, None, None, preorder_record['block_number'], preorder_record['vtxindex'], preorder_record['txid'], preorder_record)
|
|
if not rc:
|
|
log.error("FATAL: failed to save preorder for {} at ({}, {})".format(history_id, preorder_record['block_number'], preorder_record['vtxindex']))
|
|
os.abort()
|
|
|
|
# save new record
|
|
history_data = {}
|
|
history_data.update(new_record)
|
|
history_data['opcode'] = opcode
|
|
|
|
canonicalized_record = op_canonicalize_quirks(opcode, history_data, prev_rec)
|
|
canonicalized_record['opcode'] = opcode
|
|
|
|
rc = namedb_history_save(cur, opcode, history_id, history_data['address'], history_data.get('value_hash', None), block_id, vtxindex, txid, canonicalized_record)
|
|
if not rc:
|
|
log.error("FATAL: failed to save history for '%s' at (%s, %s)" % (history_id, block_id, vtxindex))
|
|
os.abort()
|
|
|
|
rc = False
|
|
if opcode in OPCODE_NAME_STATE_CREATIONS:
|
|
# name state transition
|
|
if exists:
|
|
# update existing name entry (i.e. re-registering it)
|
|
rc = namedb_name_update(cur, opcode, new_record, constraints_ignored=constraints_ignored)
|
|
else:
|
|
# insert new entry
|
|
rc = namedb_name_insert(cur, new_record)
|
|
|
|
elif opcode in OPCODE_NAMESPACE_STATE_CREATIONS:
|
|
# namespace state transition
|
|
if exists:
|
|
# update existing namespace entry (i.e. re-revealing it)
|
|
rc = namedb_namespace_update(cur, opcode, new_record, constraints_ignored=constraints_ignored)
|
|
else:
|
|
# insert new entry
|
|
rc = namedb_namespace_insert(cur, new_record)
|
|
|
|
if not rc:
|
|
log.error("FATAL: opcode is not a state-creation operation")
|
|
os.abort()
|
|
|
|
# clear the associated preorder
|
|
rc = namedb_preorder_remove( cur, preorder_record['preorder_hash'] )
|
|
if not rc:
|
|
log.error("FATAL: failed to remove preorder")
|
|
os.abort()
|
|
|
|
# success! canonicalize and preserve quirks
|
|
return canonicalized_record
|
|
|
|
|
|
def namedb_name_import_sanity_check( cur, opcode, op_data, history_id, block_id, vtxindex, prior_import, record_table):
|
|
"""
|
|
Sanity checks on a name-import:
|
|
* the opcode must match the op_data
|
|
* everything must match the record table.
|
|
* if prior_import is None, then the name shouldn't exist
|
|
* if prior_import is not None, then it must exist
|
|
|
|
Return True on success
|
|
Raise an exception on error.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
assert opcode in OPCODE_NAME_STATE_IMPORTS, "BUG: opcode '%s' does not import a name" % (opcode)
|
|
assert record_table == "name_records", "BUG: wrong table %s" % record_table
|
|
assert namedb_is_history_snapshot( op_data ), "BUG: import is incomplete"
|
|
|
|
namedb_op_sanity_check( opcode, op_data, op_data )
|
|
|
|
# must be the only such existant name, if prior_import is None
|
|
name_rec = namedb_get_name( cur, history_id, block_id )
|
|
if prior_import is None:
|
|
assert name_rec is None, "BUG: trying to import '%s' for the first time, again" % history_id
|
|
else:
|
|
assert name_rec is not None, "BUG: trying to overwrite non-existent import '%s'" % history_id
|
|
assert prior_import['name'] == history_id, "BUG: trying to overwrite import for different name '%s'" % history_id
|
|
|
|
# must actually be prior
|
|
assert prior_import['block_number'] < block_id or (prior_import['block_number'] == block_id and prior_import['vtxindex'] < vtxindex), \
|
|
"BUG: prior_import comes after op_data"
|
|
|
|
return True
|
|
|
|
|
|
def namedb_get_last_name_import(cur, name, block_id, vtxindex):
|
|
"""
|
|
Find the last name import for this name
|
|
"""
|
|
query = 'SELECT history_data FROM history WHERE history_id = ? AND (block_id < ? OR (block_id = ? AND vtxindex < ?)) ' + \
|
|
'ORDER BY block_id DESC,vtxindex DESC LIMIT 1;'
|
|
|
|
args = (name, block_id, block_id, vtxindex)
|
|
|
|
history_rows = namedb_query_execute(cur, query, args)
|
|
|
|
for row in history_rows:
|
|
history_data = json.loads(row['history_data'])
|
|
return history_data
|
|
|
|
return None
|
|
|
|
|
|
def namedb_state_create_as_import( db, opcode, new_record, block_id, vtxindex, txid, history_id, record_table, constraints_ignored=[] ):
|
|
"""
|
|
Given an operation and a new record (opcode, new_record), and point in time (block_id, vtxindex, txid)
|
|
create the initial name as an import. Does not work on namespaces.
|
|
|
|
Returns the data to snapshot on success (with all compatibility quirks preserved)
|
|
Raise an exception on failure.
|
|
|
|
DO NOT CALL THIS METHOD DIRECTLY.
|
|
"""
|
|
|
|
# sanity check: must be a name, and must be an import
|
|
if opcode not in OPCODE_NAME_STATE_IMPORTS:
|
|
log.error("FATAL: Opcode '%s' is not a state-importing operation" % opcode)
|
|
os.abort()
|
|
|
|
cur = db.cursor()
|
|
|
|
# does a previous version of this record exist?
|
|
prior_import = namedb_get_last_name_import(cur, history_id, block_id, vtxindex)
|
|
|
|
try:
|
|
|
|
# sanity check to make sure we got valid state-import data
|
|
rc = namedb_name_import_sanity_check( cur, opcode, new_record, history_id, block_id, vtxindex, prior_import, record_table )
|
|
if not rc:
|
|
raise Exception("state-import sanity check on '%s' failed" % opcode )
|
|
|
|
rc = namedb_state_mutation_sanity_check( opcode, new_record )
|
|
if not rc:
|
|
raise Exception("State mutation sanity checks failed")
|
|
|
|
assert 'opcode' not in new_record, 'BUG: opcode in new_record'
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: state-import sanity check failed")
|
|
os.abort()
|
|
|
|
cur = db.cursor()
|
|
creator_address = None
|
|
if prior_import is None:
|
|
# creating for the first time
|
|
creator_address = new_record['address']
|
|
|
|
history_data = {}
|
|
history_data.update(new_record)
|
|
history_data['opcode'] = opcode
|
|
|
|
canonicalized_record = op_canonicalize_quirks(opcode, new_record, prior_import)
|
|
canonicalized_record['opcode'] = opcode
|
|
|
|
rc = namedb_history_save(cur, opcode, history_id, creator_address, history_data.get('value_hash', None), block_id, vtxindex, txid, canonicalized_record)
|
|
if not rc:
|
|
log.error("FATAL: failed to save history snapshot for '%s' at (%s, %s)" % (history_id, block_id, vtxindex))
|
|
os.abort()
|
|
|
|
if prior_import is None:
|
|
# creating for the first time
|
|
cur = db.cursor()
|
|
rc = namedb_name_insert(cur, new_record)
|
|
|
|
else:
|
|
# updating an existing import
|
|
rc = namedb_name_update(cur, opcode, new_record, constraints_ignored=constraints_ignored)
|
|
|
|
if not rc:
|
|
log.error("FATAL: failed to execute import operation")
|
|
os.abort()
|
|
|
|
# success! canonicalize and preserve quirks
|
|
return canonicalized_record
|
|
|
|
|
|
def namedb_is_history_snapshot( history_snapshot ):
|
|
"""
|
|
Given a dict, verify that it is a history snapshot.
|
|
It must have all consensus fields.
|
|
Return True if so.
|
|
Raise an exception of it doesn't.
|
|
"""
|
|
|
|
# sanity check: each mutate field in the operation must be defined in op_data, even if it's null.
|
|
missing = []
|
|
|
|
assert 'op' in history_snapshot.keys(), "BUG: no op given"
|
|
|
|
opcode = op_get_opcode_name( history_snapshot['op'] )
|
|
assert opcode is not None, "BUG: unrecognized op '%s'" % history_snapshot['op']
|
|
|
|
consensus_fields = op_get_consensus_fields( opcode )
|
|
for field in consensus_fields:
|
|
if field not in history_snapshot.keys():
|
|
missing.append( field )
|
|
|
|
assert len(missing) == 0, ("BUG: operation '%s' is missing the following fields: %s" % (opcode, ",".join(missing)))
|
|
return True
|
|
|
|
|
|
def namedb_history_save( cur, opcode, history_id, creator_address, value_hash, block_id, vtxindex, txid, accepted_rec, history_snapshot=False ):
|
|
"""
|
|
@history_id is either the name or namespace ID
|
|
|
|
Return True on success
|
|
Raise an Exception on error
|
|
"""
|
|
|
|
assert 'op' in accepted_rec, "Malformed record at ({},{}): missing op".format(block_id, accepted_rec['vtxindex'])
|
|
|
|
op = accepted_rec['op']
|
|
|
|
record_data = op_canonicalize(opcode, accepted_rec)
|
|
record_txt = json.dumps(record_data, sort_keys=True)
|
|
|
|
history_insert = {
|
|
"txid": txid,
|
|
"history_id": history_id,
|
|
"creator_address": creator_address,
|
|
"block_id": block_id,
|
|
"vtxindex": vtxindex,
|
|
"op": op,
|
|
"opcode": opcode,
|
|
"history_data": record_txt,
|
|
'value_hash': value_hash
|
|
}
|
|
|
|
try:
|
|
query, values = namedb_insert_prepare( cur, history_insert, "history" )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to append history record for '%s' at (%s, %s)" % (history_id, block_id, vtxindex))
|
|
os.abort()
|
|
|
|
namedb_query_execute( cur, query, values )
|
|
return True
|
|
|
|
|
|
def namedb_get_blocks_with_ops( cur, history_id, start_block_id, end_block_id ):
|
|
"""
|
|
Get the block heights at which a name was affected by an operation.
|
|
Returns the list of heights.
|
|
"""
|
|
select_query = "SELECT DISTINCT name_records.block_number,history.block_id FROM history JOIN name_records ON history.history_id = name_records.name " + \
|
|
"WHERE name_records.name = ? AND ((name_records.block_number >= ? OR history.block_id >= ?) AND (name_records.block_number < ? OR history.block_id < ?));"
|
|
args = (history_id, start_block_id, start_block_id, end_block_id, end_block_id)
|
|
|
|
history_rows = namedb_query_execute( cur, select_query, args )
|
|
ret = []
|
|
|
|
for r in history_rows:
|
|
if r['block_number'] not in ret:
|
|
ret.append(r['block_number'])
|
|
|
|
if r['block_id'] not in ret:
|
|
ret.append(r['block_id'])
|
|
|
|
ret.sort()
|
|
return ret
|
|
|
|
|
|
def namedb_get_history_rows( cur, history_id, offset=None, count=None ):
|
|
"""
|
|
Get the history for a name or namespace from the history table.
|
|
Use offset/count if given.
|
|
"""
|
|
ret = []
|
|
select_query = "SELECT * FROM history WHERE history_id = ? ORDER BY block_id ASC, vtxindex ASC"
|
|
args = (history_id,)
|
|
|
|
if count is not None:
|
|
select_query += " LIMIT ?"
|
|
args += (count,)
|
|
|
|
if offset is not None:
|
|
select_query += " OFFSET ?"
|
|
args += (offset,)
|
|
|
|
select_query += ";"
|
|
|
|
history_rows = namedb_query_execute( cur, select_query, args)
|
|
for r in history_rows:
|
|
rd = dict(r)
|
|
ret.append(rd)
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_num_history_rows( cur, history_id ):
|
|
"""
|
|
Get the history for a name or namespace from the history table.
|
|
Use offset/count if given.
|
|
"""
|
|
ret = []
|
|
select_query = "SELECT COUNT(*) FROM history WHERE history_id = ? ORDER BY block_id ASC, vtxindex ASC;"
|
|
args = (history_id,)
|
|
|
|
count = namedb_select_count_rows( cur, select_query, args )
|
|
return count
|
|
|
|
|
|
def namedb_get_history( cur, history_id ):
|
|
"""
|
|
Get all of the history for a name or namespace.
|
|
Returns a dict keyed by block heights, paired to lists of changes (see namedb_history_extract)
|
|
"""
|
|
|
|
# get history in increasing order by block_id and then vtxindex
|
|
history_rows = namedb_get_history_rows( cur, history_id )
|
|
return namedb_history_extract( history_rows )
|
|
|
|
|
|
def namedb_history_extract( history_rows ):
|
|
"""
|
|
Given the rows of history for a name, collapse
|
|
them into a history dictionary.
|
|
Return a dict of:
|
|
{
|
|
block_id: [
|
|
{ ... historical copy ...
|
|
txid:
|
|
vtxindex:
|
|
op:
|
|
opcode:
|
|
}, ...
|
|
],
|
|
...
|
|
}
|
|
"""
|
|
|
|
history = {}
|
|
for history_row in history_rows:
|
|
|
|
block_id = history_row['block_id']
|
|
data_json = history_row['history_data']
|
|
hist = json.loads( data_json )
|
|
|
|
hist['opcode'] = op_get_opcode_name( hist['op'] )
|
|
hist = op_decanonicalize(hist['opcode'], hist)
|
|
|
|
if history.has_key( block_id ):
|
|
history[ block_id ].append( hist )
|
|
else:
|
|
history[ block_id ] = [ hist ]
|
|
|
|
return history
|
|
|
|
|
|
def namedb_flatten_history( 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 namedb_get_namespace( cur, namespace_id, current_block, include_expired=False, include_history=True, only_revealed=True):
|
|
"""
|
|
Get a namespace (revealed or ready) and optionally its history.
|
|
Only return an expired namespace if asked.
|
|
If current_block is None, any namespace is returned (expired or not)
|
|
If current_block is not None and only_revealed is False, then a namespace can be returned before it was revealed.
|
|
-- if include_expired is False, then a namespace can be returned only if current_block is less than the expire block
|
|
-- otherwise, any namespace can be returned
|
|
"""
|
|
|
|
include_expired_query = ""
|
|
include_expired_args = ()
|
|
|
|
min_age_query = ""
|
|
min_age_args = ()
|
|
|
|
if only_revealed:
|
|
# requier lower bound on age
|
|
min_age_query = " AND namespaces.reveal_block <= ?"
|
|
min_age_args = (current_block,)
|
|
|
|
if not include_expired:
|
|
assert current_block is not None
|
|
# require upper bound on age
|
|
include_expired_query = " AND ? < namespaces.reveal_block + ?"
|
|
include_expired_args = (current_block, NAMESPACE_REVEAL_EXPIRE)
|
|
|
|
if current_block is None:
|
|
# no bounds on age
|
|
min_age_query = ""
|
|
min_age_args = ()
|
|
|
|
select_query = "SELECT * FROM namespaces WHERE namespace_id = ? AND " + \
|
|
"((op = ?) OR (op = ? %s %s))" % (min_age_query, include_expired_query)
|
|
|
|
args = (namespace_id, NAMESPACE_READY, NAMESPACE_REVEAL) + min_age_args + include_expired_args
|
|
|
|
log.debug(namedb_format_query(select_query, args))
|
|
|
|
namespace_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
namespace_row = namespace_rows.fetchone()
|
|
if namespace_row is None:
|
|
# no such namespace
|
|
return None
|
|
|
|
namespace = {}
|
|
namespace.update( namespace_row )
|
|
|
|
if include_history:
|
|
hist = namedb_get_history( cur, namespace_id )
|
|
namespace['history'] = hist
|
|
|
|
namespace = op_decanonicalize(op_get_opcode_name(namespace['op']), namespace)
|
|
return namespace
|
|
|
|
|
|
def namedb_get_namespace_by_preorder_hash( cur, preorder_hash, include_history=True ):
|
|
"""
|
|
Get a namespace by its preorder hash (regardless of whether or not it was expired.)
|
|
"""
|
|
|
|
select_query = "SELECT * FROM namespaces WHERE preorder_hash = ?;"
|
|
namespace_rows = namedb_query_execute( cur, select_query, (preorder_hash,))
|
|
|
|
namespace_row = namespace_rows.fetchone()
|
|
if namespace_row is None:
|
|
# no such namespace
|
|
return None
|
|
|
|
namespace = {}
|
|
namespace.update( namespace_row )
|
|
|
|
if include_history:
|
|
hist = namedb_get_history( cur, namespace['namespace_id'] )
|
|
namespace['history'] = hist
|
|
|
|
namespace = op_decanonicalize(op_get_opcode_name(namespace['op']), namespace)
|
|
return namespace
|
|
|
|
|
|
def namedb_get_name_by_preorder_hash( cur, preorder_hash, include_history=True ):
|
|
"""
|
|
Get a name by its preorder hash (regardless of whether or not it was expired or revoked.)
|
|
"""
|
|
|
|
select_query = "SELECT * FROM name_records WHERE preorder_hash = ?;"
|
|
name_rows = namedb_query_execute( cur, select_query, (preorder_hash,))
|
|
|
|
name_row = name_rows.fetchone()
|
|
if name_row is None:
|
|
# no such preorder
|
|
return None
|
|
|
|
namerec = {}
|
|
namerec.update( name_row )
|
|
|
|
if include_history:
|
|
hist = namedb_get_history( cur, namerec['name'] )
|
|
namerec['history'] = hist
|
|
|
|
return namerec
|
|
|
|
|
|
def namedb_select_where_unexpired_names(current_block, only_registered=True):
|
|
"""
|
|
Generate part of a WHERE clause that selects from name records joined with namespaces
|
|
(or projections of them) that are not expired.
|
|
|
|
Also limit to names that are registered at this block, if only_registered=True.
|
|
If only_registered is False, then as long as current_block is before the expire block, then the name will be returned (but the name may not have existed at that block)
|
|
"""
|
|
|
|
unexpired_query_fragment = "(" + \
|
|
"(" + \
|
|
"namespaces.op = ? AND " + \
|
|
"(" + \
|
|
"(namespaces.ready_block + ((namespaces.lifetime * namespace_lifetime_multiplier(?, namespaces.namespace_id)) + namespace_lifetime_grace_period(?, namespaces.namespace_id)) > ?) OR " + \
|
|
"(name_records.last_renewed + ((namespaces.lifetime * namespace_lifetime_multiplier(?, namespaces.namespace_id)) + namespace_lifetime_grace_period(?, namespaces.namespace_id)) >= ?)" + \
|
|
")" + \
|
|
") OR " + \
|
|
"(" + \
|
|
"namespaces.op = ? AND namespaces.reveal_block <= ? AND ? < namespaces.reveal_block + ?" + \
|
|
")" + \
|
|
")"
|
|
|
|
unexpired_query_args = (NAMESPACE_READY,
|
|
current_block, current_block, current_block,
|
|
current_block, current_block, current_block,
|
|
NAMESPACE_REVEAL, current_block, current_block, NAMESPACE_REVEAL_EXPIRE)
|
|
|
|
if only_registered:
|
|
# also limit to only names registered before this block
|
|
unexpired_query_fragment = '(name_records.first_registered <= ? AND {})'.format(unexpired_query_fragment)
|
|
unexpired_query_args = (current_block,) + unexpired_query_args
|
|
|
|
return (unexpired_query_fragment, unexpired_query_args)
|
|
|
|
|
|
def namedb_get_name(cur, name, current_block, include_expired=False, include_history=True, only_registered=True):
|
|
"""
|
|
Get a name and all of its history. Note: will return a revoked name
|
|
Return the name + history on success
|
|
Return None if the name doesn't exist, or is expired (NOTE: will return a revoked name)
|
|
"""
|
|
|
|
if not include_expired:
|
|
|
|
unexpired_fragment, unexpired_args = namedb_select_where_unexpired_names(current_block, only_registered=only_registered)
|
|
select_query = "SELECT name_records.* FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE name = ? AND " + unexpired_fragment + ";"
|
|
args = (name, ) + unexpired_args
|
|
|
|
else:
|
|
select_query = "SELECT * FROM name_records WHERE name = ?;"
|
|
args = (name,)
|
|
|
|
# log.debug(namedb_format_query(select_query, args))
|
|
|
|
name_rows = namedb_query_execute( cur, select_query, args )
|
|
name_row = name_rows.fetchone()
|
|
if name_row is None:
|
|
# no such name
|
|
return None
|
|
|
|
name_rec = {}
|
|
name_rec.update( name_row )
|
|
|
|
if include_history:
|
|
name_history = namedb_get_history( cur, name )
|
|
name_rec['history'] = name_history
|
|
|
|
return name_rec
|
|
|
|
|
|
def namedb_get_name_DID_info(cur, name):
|
|
"""
|
|
Given a name and a DB cursor, find out its DID info.
|
|
Returns {'name_type': ..., 'address': ..., 'index': ...} on success
|
|
Return None if there is no such name
|
|
"""
|
|
# get the first creator address
|
|
sql = "SELECT name_records.name,history.creator_address FROM name_records JOIN history ON name_records.name = history.history_id WHERE name = ? AND creator_address IS NOT NULL ORDER BY history.block_id,history.vtxindex LIMIT 1;"
|
|
args = (name,)
|
|
|
|
rows = namedb_query_execute(cur, sql, args)
|
|
row = rows.fetchone()
|
|
if row is None:
|
|
return None
|
|
|
|
creator_address = row['creator_address']
|
|
num_rows = namedb_get_num_historic_names_by_address(cur, creator_address)
|
|
if num_rows == 0:
|
|
return None
|
|
|
|
offset = 0
|
|
addr_index = 0
|
|
while True:
|
|
rows = namedb_get_historic_names_by_address(cur, creator_address, offset=offset, count=100)
|
|
offset += 100
|
|
|
|
if len(rows) == 0:
|
|
# done searching
|
|
return None
|
|
|
|
for row in rows:
|
|
if row['name'] == name:
|
|
# found!
|
|
return {'name_type': 'name', 'address': str(creator_address), 'index': addr_index}
|
|
|
|
addr_index += 1
|
|
|
|
return None
|
|
|
|
|
|
def namedb_get_record_states_at(cur, history_id, block_number):
|
|
"""
|
|
Get the state(s) that the given history record was in at a given block height.
|
|
Normally, this is one state (i.e. if a name was registered at block 8, then it is in a NAME_REGISTRATION state in block 10)
|
|
|
|
However, if the record changed at this block, then this method returns all states the record passed through.
|
|
|
|
Returns an array of record states
|
|
"""
|
|
query = 'SELECT block_id,history_data FROM history WHERE history_id = ? AND block_id == ? ORDER BY block_id DESC,vtxindex DESC'
|
|
args = (history_id, block_number)
|
|
history_rows = namedb_query_execute(cur, query, args)
|
|
ret = []
|
|
|
|
for row in history_rows:
|
|
history_data = simplejson.loads(row['history_data'])
|
|
ret.append(history_data)
|
|
|
|
if len(ret) > 0:
|
|
# record changed in this block
|
|
return ret
|
|
|
|
# if the name did not change in this block, then find the last version of the name
|
|
query = 'SELECT block_id,history_data FROM history WHERE history_id = ? AND block_id < ? ORDER BY block_id DESC,vtxindex DESC LIMIT 1'
|
|
args = (history_id, block_number)
|
|
history_rows = namedb_query_execute(cur, query, args)
|
|
|
|
for row in history_rows:
|
|
history_data = simplejson.loads(row['history_data'])
|
|
ret.append(history_data)
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_name_at(cur, name, block_number, include_expired=False):
|
|
"""
|
|
Get the sequence of states that a name record was in at a particular block height.
|
|
There can be more than one if the name changed during the block.
|
|
|
|
Returns only unexpired names by default. Can return expired names with include_expired=True
|
|
Returns None if this name does not exist at this block height.
|
|
"""
|
|
if not include_expired:
|
|
# don't return anything if this name is expired.
|
|
# however, we don't care if the name hasn't been created as of this block_number either, since we might return its preorder (hence only_registered=False)
|
|
name_rec = namedb_get_name(cur, name, block_number, include_expired=False, include_history=False, only_registered=False)
|
|
if name_rec is None:
|
|
# expired at this block.
|
|
return None
|
|
|
|
history_rows = namedb_get_record_states_at(cur, name, block_number)
|
|
if len(history_rows) == 0:
|
|
# doesn't exist
|
|
return None
|
|
else:
|
|
return history_rows
|
|
|
|
|
|
def namedb_get_namespace_at(cur, namespace_id, block_number, include_expired=False):
|
|
"""
|
|
Get the sequence of states that a namespace record was in at a particular block height.
|
|
There can be more than one if the namespace changed durnig the block.
|
|
|
|
Returns only unexpired namespaces by default. Can return expired namespaces with include_expired=True
|
|
"""
|
|
if not include_expired:
|
|
# don't return anything if the namespace was expired at this block.
|
|
# (but do return something here even if the namespace was created after this block, so we can potentially pick up its preorder (hence only_revealed=False))
|
|
namespace_rec = namedb_get_namespace(cur, namespace_id, block_number, include_expired=False, include_history=False, only_revealed=False)
|
|
if namespace_rec is None:
|
|
# expired at this block
|
|
return None
|
|
|
|
history_rows = namedb_get_record_states_at(cur, namespace_id, block_number)
|
|
if len(history_rows) == 0:
|
|
# doesn't exist yet
|
|
return None
|
|
else:
|
|
return history_rows
|
|
|
|
|
|
def namedb_get_preorder(cur, preorder_hash, current_block_number, include_expired=False, expiry_time=None):
|
|
"""
|
|
Get a preorder record by hash.
|
|
If include_expired is set, then so must expiry_time
|
|
Return None if not found.
|
|
"""
|
|
|
|
select_query = None
|
|
args = None
|
|
|
|
if include_expired:
|
|
select_query = "SELECT * FROM preorders WHERE preorder_hash = ?;"
|
|
args = (preorder_hash,)
|
|
|
|
else:
|
|
assert expiry_time is not None, "expiry_time is required with include_expired"
|
|
select_query = "SELECT * FROM preorders WHERE preorder_hash = ? AND block_number < ?;"
|
|
args = (preorder_hash, expiry_time + current_block_number)
|
|
|
|
preorder_rows = namedb_query_execute( cur, select_query, (preorder_hash,))
|
|
preorder_row = preorder_row.fetchone()
|
|
if preorder_row is None:
|
|
# no such preorder
|
|
return None
|
|
|
|
preorder_rec = {}
|
|
preorder_rec.update( preorder_row )
|
|
|
|
return preorder_rec
|
|
|
|
|
|
def namedb_get_names_owned_by_address( cur, address, current_block ):
|
|
"""
|
|
Get the list of non-expired, non-revoked names owned by an address.
|
|
Only works if there is a *singular* address for the name.
|
|
"""
|
|
|
|
unexpired_fragment, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
|
|
select_query = "SELECT * FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE name_records.address = ? AND name_records.revoked = 0 AND " + unexpired_fragment + ";"
|
|
args = (address,) + unexpired_args
|
|
|
|
name_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
names = []
|
|
for name_row in name_rows:
|
|
names.append( name_row['name'] )
|
|
|
|
if len(names) == 0:
|
|
return None
|
|
else:
|
|
return names
|
|
|
|
|
|
def namedb_get_num_historic_names_by_address( cur, address ):
|
|
"""
|
|
Get the number of names owned by an address throughout history
|
|
"""
|
|
|
|
select_query = "SELECT COUNT(*) FROM name_records JOIN history ON name_records.name = history.history_id " + \
|
|
"WHERE history.creator_address = ?;"
|
|
|
|
args = (address,)
|
|
|
|
count = namedb_select_count_rows( cur, select_query, args )
|
|
return count
|
|
|
|
|
|
def namedb_get_historic_names_by_address( cur, address, offset=None, count=None ):
|
|
"""
|
|
Get the list of all names ever owned by this address (except the current one), ordered by creation date.
|
|
Return a list of {'name': ..., 'block_id': ..., 'vtxindex': ...}}
|
|
"""
|
|
|
|
query = "SELECT name_records.name,history.block_id,history.vtxindex FROM name_records JOIN history ON name_records.name = history.history_id " + \
|
|
"WHERE history.creator_address = ? ORDER BY history.block_id, history.vtxindex "
|
|
|
|
args = (address,)
|
|
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate( offset=offset, count=count )
|
|
query += offset_count_query + ";"
|
|
args += offset_count_args
|
|
|
|
name_rows = namedb_query_execute( cur, query, args )
|
|
|
|
names = []
|
|
for name_row in name_rows:
|
|
info = {
|
|
'name': name_row['name'],
|
|
'block_id': name_row['block_id'],
|
|
'vtxindex': name_row['vtxindex']
|
|
}
|
|
|
|
names.append( info )
|
|
|
|
if len(names) == 0:
|
|
return None
|
|
else:
|
|
return names
|
|
|
|
|
|
def namedb_offset_count_predicate( offset=None, count=None ):
|
|
"""
|
|
Make an offset/count predicate
|
|
even if offset=None or count=None.
|
|
|
|
Return (query, args)
|
|
"""
|
|
offset_count_query = ""
|
|
offset_count_args = ()
|
|
|
|
if count is not None:
|
|
offset_count_query += "LIMIT ? "
|
|
offset_count_args += (count,)
|
|
|
|
if count is not None and offset is not None:
|
|
offset_count_query += "OFFSET ? "
|
|
offset_count_args += (offset,)
|
|
|
|
return (offset_count_query, offset_count_args)
|
|
|
|
|
|
def namedb_select_count_rows( cur, query, args, count_column='COUNT(*)' ):
|
|
"""
|
|
Execute a SELECT COUNT(*) ... query
|
|
and return the number of rows.
|
|
"""
|
|
count_rows = namedb_query_execute( cur, query, args )
|
|
count = 0
|
|
for r in count_rows:
|
|
count = r[count_column]
|
|
break
|
|
|
|
return count
|
|
|
|
|
|
def namedb_get_all_ops_at(db, block_id, offset=None, count=None):
|
|
"""
|
|
Get the states that each name and namespace record
|
|
passed through in the given block.
|
|
|
|
Return the list of prior record states, ordered by vtxindex.
|
|
"""
|
|
assert (count is None and offset is None) or (count is not None and offset is not None), 'Invalid arguments: expect both offset/count or neither offset/count'
|
|
|
|
ret = []
|
|
cur = db.cursor()
|
|
|
|
# how many preorders at this block?
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate(offset=offset, count=count)
|
|
|
|
preorder_count_rows_query = "SELECT COUNT(*) FROM preorders WHERE block_number = ? " + " " + offset_count_query + ";"
|
|
preorder_count_rows_args = (block_id,) + offset_count_args
|
|
|
|
# log.debug(namedb_format_query(preorder_count_rows_query, preorder_count_rows_args))
|
|
|
|
num_preorders = namedb_select_count_rows(cur, preorder_count_rows_query, preorder_count_rows_args)
|
|
|
|
# get preorders at this block
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate(offset=offset, count=count)
|
|
|
|
preorder_rows_query = "SELECT * FROM preorders WHERE block_number = ? " + " " + offset_count_query + ";"
|
|
preorder_rows_args = (block_id,) + offset_count_args
|
|
|
|
# log.debug(namedb_format_query(preorder_rows_query, preorder_rows_args))
|
|
|
|
preorder_rows = namedb_query_execute(cur, preorder_rows_query, preorder_rows_args)
|
|
|
|
cnt = 0
|
|
for preorder in preorder_rows:
|
|
preorder_rec = {}
|
|
preorder_rec.update( preorder )
|
|
|
|
ret.append( preorder_rec )
|
|
cnt += 1
|
|
|
|
log.debug("{} preorders created at {}".format(cnt, block_id))
|
|
|
|
# don't return too many rows, and slide down the offset window
|
|
if count is not None and offset is not None:
|
|
offset = max(0, offset - num_preorders)
|
|
count -= num_preorders
|
|
if count <= 0:
|
|
# done!
|
|
return ret
|
|
|
|
# get all other operations at this block
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate(offset=offset, count=count)
|
|
query = "SELECT history_data FROM history WHERE block_id = ? ORDER BY vtxindex " + offset_count_query + ";"
|
|
args = (block_id,) + offset_count_args
|
|
|
|
# log.debug(namedb_format_query(query, args))
|
|
|
|
rows_result = namedb_query_execute(cur, query, args)
|
|
|
|
# extract rows
|
|
cnt = 0
|
|
for r in rows_result:
|
|
history_data_str = r['history_data']
|
|
|
|
try:
|
|
history_data = json.loads(history_data_str)
|
|
except Exception as e:
|
|
log.exception(e)
|
|
log.error("FATAL: corrupt JSON string '{}'".format(history_data_str))
|
|
os.abort()
|
|
|
|
ret.append(history_data)
|
|
cnt += 1
|
|
|
|
log.debug("{} non-preorder operations at {}".format(cnt, block_id))
|
|
return ret
|
|
|
|
|
|
def namedb_get_num_ops_at( db, block_id ):
|
|
"""
|
|
Get the number of operations that occurred at a particular block.
|
|
"""
|
|
cur = db.cursor()
|
|
|
|
# preorders at this block
|
|
preorder_count_rows_query = "SELECT COUNT(*) FROM preorders WHERE block_number = ?;"
|
|
preorder_count_rows_args = (block_id,)
|
|
|
|
num_preorders = namedb_select_count_rows(cur, preorder_count_rows_query, preorder_count_rows_args)
|
|
|
|
# committed operations at this block
|
|
query = "SELECT COUNT(*) FROM history WHERE block_id = ?;"
|
|
args = (block_id,)
|
|
|
|
rows_result = namedb_query_execute(cur, query, args)
|
|
|
|
count = 0
|
|
for r in rows_result:
|
|
count = r['COUNT(*)']
|
|
break
|
|
|
|
log.debug("{} preorders; {} history rows at {}".format(num_preorders, count, block_id))
|
|
return count + num_preorders
|
|
|
|
|
|
def namedb_get_num_names( cur, current_block, include_expired=False ):
|
|
"""
|
|
Get the number of names that exist at the current block
|
|
"""
|
|
unexpired_query = ""
|
|
unexpired_args = ()
|
|
|
|
if not include_expired:
|
|
# count all names, including expired ones
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
unexpired_query = 'WHERE {}'.format(unexpired_query)
|
|
|
|
query = "SELECT COUNT(name_records.name) FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + unexpired_query + ";"
|
|
args = unexpired_args
|
|
|
|
num_rows = namedb_select_count_rows( cur, query, args, count_column='COUNT(name_records.name)' )
|
|
return num_rows
|
|
|
|
|
|
def namedb_get_all_names( cur, current_block, offset=None, count=None, include_expired=False ):
|
|
"""
|
|
Get a list of all names in the database, optionally
|
|
paginated with offset and count. Exclude expired names. Include revoked names.
|
|
"""
|
|
|
|
unexpired_query = ""
|
|
unexpired_args = ()
|
|
|
|
if not include_expired:
|
|
# all names, including expired ones
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
unexpired_query = 'WHERE {}'.format(unexpired_query)
|
|
|
|
query = "SELECT name FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + unexpired_query
|
|
args = unexpired_args
|
|
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate( offset=offset, count=count )
|
|
query += offset_count_query + ";"
|
|
args += offset_count_args
|
|
|
|
name_rows = namedb_query_execute( cur, query, tuple(args) )
|
|
ret = []
|
|
for name_row in name_rows:
|
|
rec = {}
|
|
rec.update( name_row )
|
|
ret.append( rec['name'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_num_names_in_namespace( cur, namespace_id, current_block ):
|
|
"""
|
|
Get the number of names in a given namespace
|
|
"""
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
|
|
query = "SELECT COUNT(name_records.name) FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id WHERE name_records.namespace_id = ? AND " + unexpired_query + " ORDER BY name;"
|
|
args = (namespace_id,) + unexpired_args
|
|
|
|
num_rows = namedb_select_count_rows( cur, query, args, count_column='COUNT(name_records.name)' )
|
|
return num_rows
|
|
|
|
|
|
def namedb_get_names_in_namespace( cur, namespace_id, current_block, offset=None, count=None ):
|
|
"""
|
|
Get a list of all names in a namespace, optionally
|
|
paginated with offset and count. Exclude expired names
|
|
"""
|
|
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
|
|
query = "SELECT name FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id WHERE name_records.namespace_id = ? AND " + unexpired_query + " ORDER BY name "
|
|
args = (namespace_id,) + unexpired_args
|
|
|
|
offset_count_query, offset_count_args = namedb_offset_count_predicate( offset=offset, count=count )
|
|
query += offset_count_query + ";"
|
|
args += offset_count_args
|
|
|
|
name_rows = namedb_query_execute( cur, query, tuple(args) )
|
|
ret = []
|
|
for name_row in name_rows:
|
|
rec = {}
|
|
rec.update( name_row )
|
|
ret.append( rec['name'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_all_namespace_ids( cur ):
|
|
"""
|
|
Get a list of all READY namespace IDs.
|
|
"""
|
|
|
|
query = "SELECT namespace_id FROM namespaces WHERE op = ?;"
|
|
args = (NAMESPACE_READY,)
|
|
|
|
namespace_rows = namedb_query_execute( cur, query, args )
|
|
ret = []
|
|
for namespace_row in namespace_rows:
|
|
ret.append( namespace_row['namespace_id'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_all_preordered_namespace_hashes( cur, current_block ):
|
|
"""
|
|
Get a list of all preordered namespace hashes that haven't expired yet.
|
|
Used for testing
|
|
"""
|
|
query = "SELECT preorder_hash FROM preorders WHERE op = ? AND block_number >= ? AND block_number < ?;"
|
|
args = (NAMESPACE_PREORDER, current_block, current_block + NAMESPACE_PREORDER_EXPIRE )
|
|
|
|
namespace_rows = namedb_query_execute( cur, query, args )
|
|
ret = []
|
|
for namespace_row in namespace_rows:
|
|
ret.append( namespace_row['preorder_hash'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_all_revealed_namespace_ids( self, current_block ):
|
|
"""
|
|
Get all non-expired revealed namespaces.
|
|
"""
|
|
|
|
query = "SELECT namespace_id FROM namespaces WHERE op = ? AND reveal_block < ?;"
|
|
args = (NAMESPACE_REVEAL, current_block + NAMESPACE_REVEAL_EXPIRE )
|
|
|
|
namespace_rows = namedb_query_execute( cur, query, args )
|
|
ret = []
|
|
for namespace_row in namespace_rows:
|
|
ret.append( namespace_row['namespace_id'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_all_importing_namespace_hashes( self, current_block ):
|
|
"""
|
|
Get the list of all non-expired preordered and revealed namespace hashes.
|
|
"""
|
|
|
|
query = "SELECT preorder_hash FROM namespaces WHERE (op = ? AND reveal_block < ?) OR (op = ? AND block_number < ?);"
|
|
args = (NAMESPACE_REVEAL, current_block + NAMESPACE_REVEAL_EXPIRE, NAMESPACE_PREORDER, current_block + NAMESPACE_PREORDER_EXPIRE )
|
|
|
|
namespace_rows = namedb_query_execute( cur, query, args )
|
|
ret = []
|
|
for namespace_row in namespace_rows:
|
|
ret.append( namespace_row['preorder_hash'] )
|
|
|
|
return ret
|
|
|
|
|
|
def namedb_get_names_by_sender( cur, sender, current_block ):
|
|
"""
|
|
Given a sender pubkey script, find all the non-expired non-revoked names owned by it.
|
|
Return None if the sender owns no names.
|
|
"""
|
|
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
|
|
query = "SELECT name_records.name FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE name_records.sender = ? AND name_records.revoked = 0 AND " + unexpired_query + ";"
|
|
|
|
args = (sender,) + unexpired_args
|
|
|
|
name_rows = namedb_query_execute( cur, query, args )
|
|
names = []
|
|
|
|
for name_row in name_rows:
|
|
names.append( name_row['name'] )
|
|
|
|
return names
|
|
|
|
|
|
def namedb_get_name_preorder( db, preorder_hash, current_block ):
|
|
"""
|
|
Get a (singular) name preorder record outstanding at the given block, given the preorder hash.
|
|
NOTE: returns expired preorders.
|
|
|
|
Return the preorder record on success.
|
|
Return None if not found.
|
|
"""
|
|
|
|
select_query = "SELECT * FROM preorders WHERE preorder_hash = ? AND op = ? AND block_number < ?;"
|
|
args = (preorder_hash, NAME_PREORDER, current_block + NAME_PREORDER_EXPIRE)
|
|
|
|
cur = db.cursor()
|
|
preorder_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
preorder_row = preorder_rows.fetchone()
|
|
if preorder_row is None:
|
|
# no such preorder
|
|
return None
|
|
|
|
preorder_rec = {}
|
|
preorder_rec.update( preorder_row )
|
|
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( current_block )
|
|
|
|
# make sure that the name doesn't already exist
|
|
select_query = "SELECT name_records.preorder_hash " + \
|
|
"FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE name_records.preorder_hash = ? AND " + \
|
|
unexpired_query + ";"
|
|
|
|
args = (preorder_hash,) + unexpired_args
|
|
|
|
cur = db.cursor()
|
|
nm_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
nm_row = nm_rows.fetchone()
|
|
if nm_row is not None:
|
|
# name with this preorder exists
|
|
return None
|
|
|
|
return preorder_rec
|
|
|
|
|
|
def namedb_get_namespace_preorder( db, namespace_preorder_hash, current_block ):
|
|
"""
|
|
Get a namespace preorder, given its hash.
|
|
|
|
Return the preorder record on success.
|
|
Return None if not found, or if it expired, or if the namespace was revealed or readied.
|
|
"""
|
|
|
|
cur = db.cursor()
|
|
select_query = "SELECT * FROM preorders WHERE preorder_hash = ? AND op = ? AND block_number < ?;"
|
|
args = (namespace_preorder_hash, NAMESPACE_PREORDER, current_block + NAMESPACE_PREORDER_EXPIRE)
|
|
preorder_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
preorder_row = preorder_rows.fetchone()
|
|
if preorder_row is None:
|
|
# no such preorder
|
|
return None
|
|
|
|
preorder_rec = {}
|
|
preorder_rec.update( preorder_row )
|
|
|
|
# make sure that the namespace doesn't already exist
|
|
cur = db.cursor()
|
|
select_query = "SELECT preorder_hash FROM namespaces WHERE preorder_hash = ? AND ((op = ?) OR (op = ? AND reveal_block < ?));"
|
|
args = (namespace_preorder_hash, NAMESPACE_READY, NAMESPACE_REVEAL, current_block + NAMESPACE_REVEAL_EXPIRE)
|
|
ns_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
ns_row = ns_rows.fetchone()
|
|
if ns_row is not None:
|
|
# exists
|
|
return None
|
|
|
|
return preorder_rec
|
|
|
|
|
|
def namedb_get_namespace_reveal( cur, namespace_id, current_block, include_history=True ):
|
|
"""
|
|
Get a namespace reveal, and optionally its history, given its namespace ID.
|
|
Only return a namespace record if:
|
|
* it is not ready
|
|
* it is not expired
|
|
"""
|
|
|
|
select_query = "SELECT * FROM namespaces WHERE namespace_id = ? AND op = ? AND reveal_block <= ? AND ? < reveal_block + ?;"
|
|
args = (namespace_id, NAMESPACE_REVEAL, current_block, current_block, NAMESPACE_REVEAL_EXPIRE)
|
|
|
|
namespace_reveal_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
namespace_reveal_row = namespace_reveal_rows.fetchone()
|
|
if namespace_reveal_row is None:
|
|
# no such reveal
|
|
return None
|
|
|
|
reveal_rec = {}
|
|
reveal_rec.update( namespace_reveal_row )
|
|
|
|
if include_history:
|
|
hist = namedb_get_history( cur, namespace_id )
|
|
reveal_rec['history'] = hist
|
|
|
|
reveal_rec = op_decanonicalize('NAMESPACE_REVEAL', reveal_rec)
|
|
return reveal_rec
|
|
|
|
|
|
def namedb_get_namespace_ready( cur, namespace_id, include_history=True ):
|
|
"""
|
|
Get a ready namespace, and optionally its history.
|
|
Only return a namespace if it is ready.
|
|
"""
|
|
|
|
select_query = "SELECT * FROM namespaces WHERE namespace_id = ? AND op = ?;"
|
|
namespace_rows = namedb_query_execute( cur, select_query, (namespace_id, NAMESPACE_READY))
|
|
|
|
namespace_row = namespace_rows.fetchone()
|
|
if namespace_row is None:
|
|
# no such namespace
|
|
return None
|
|
|
|
namespace = {}
|
|
namespace.update( namespace_row )
|
|
|
|
if include_history:
|
|
hist = namedb_get_history( cur, namespace_id )
|
|
namespace['history'] = hist
|
|
|
|
namespace = op_decanonicalize('NAMESPACE_READY', namespace)
|
|
return namespace
|
|
|
|
|
|
def namedb_get_name_from_name_hash128( cur, name_hash128, block_number ):
|
|
"""
|
|
Given the hexlified 128-bit hash of a name, get the name.
|
|
"""
|
|
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( block_number )
|
|
|
|
select_query = "SELECT name FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE name_hash128 = ? AND revoked = 0 AND " + unexpired_query + ";"
|
|
|
|
args = (name_hash128,) + unexpired_args
|
|
name_rows = namedb_query_execute( cur, select_query, args )
|
|
|
|
name_row = name_rows.fetchone()
|
|
if name_row is None:
|
|
# no such namespace
|
|
return None
|
|
|
|
return name_row['name']
|
|
|
|
|
|
def namedb_get_names_with_value_hash( cur, value_hash, block_number ):
|
|
"""
|
|
Get the names with the given value hash. Only includes current, non-revoked names.
|
|
Return None if there are no names.
|
|
"""
|
|
|
|
unexpired_query, unexpired_args = namedb_select_where_unexpired_names( block_number )
|
|
select_query = "SELECT name FROM name_records JOIN namespaces ON name_records.namespace_id = namespaces.namespace_id " + \
|
|
"WHERE value_hash = ? AND revoked = 0 AND " + unexpired_query + ";"
|
|
|
|
args = (value_hash,) + unexpired_args
|
|
name_rows = namedb_query_execute( cur, select_query, args )
|
|
names = []
|
|
|
|
for name_row in name_rows:
|
|
names.append( name_row['name'] )
|
|
|
|
if len(names) == 0:
|
|
return None
|
|
else:
|
|
return names
|
|
|
|
|
|
def namedb_get_value_hash_txids(cur, value_hash):
|
|
"""
|
|
Get the list of txs that sent this value hash, ordered by block and vtxindex
|
|
"""
|
|
query = 'SELECT txid FROM history WHERE value_hash = ? ORDER BY block_id,vtxindex;'
|
|
args = (value_hash,)
|
|
|
|
rows = namedb_query_execute(cur, query, args)
|
|
txids = []
|
|
|
|
for r in rows:
|
|
# present
|
|
txid = str(r['txid'])
|
|
txids.append(txid)
|
|
|
|
return txids
|
|
|
|
|
|
def namedb_get_num_block_vtxs( cur, block_number ):
|
|
"""
|
|
How many virtual transactions were processed for this block?
|
|
"""
|
|
select_query = "SELECT vtxindex FROM history WHERE history_id = ?;"
|
|
args = (block_number,)
|
|
|
|
rows = namedb_query_execute( cur, select_query, args )
|
|
count = 0
|
|
for r in rows:
|
|
count += 1
|
|
|
|
return count
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# basic unit tests
|
|
import random
|
|
import pybitcoin
|
|
|
|
path = "/tmp/namedb.sqlite"
|
|
if not os.path.exists( path ):
|
|
db = namedb_create( path )
|
|
else:
|
|
db = namedb_open( path )
|
|
|
|
name = "test%s.test" % random.randint( 0, 2**32 )
|
|
sender = "76a9147144b3fef9fe537e2445f1c0dfb4ce007c51461288ac"
|
|
sender_pubkey = "046a6582a6566aa4059b7361536e7e4ac3df4d77bf6e843c4c8207eaa12e0ca19e15fc59c959b4a5d6d1de975ab059d9255a795dd57b9c78656a070ea5002efe87"
|
|
sender_address = "1BKufFedDrueBBFBXtiATB2PSdsBGZxf3N"
|
|
recipient = "76a914d3d4a11953ce8ba01b08548997830c11b1ad9a7288ac"
|
|
recipient_pubkey = "04f52b0c1558202cb4403faacb6a74ad8a0d23538f448184b1c8ce80a0325aad9af0877061c2fd72cebec8524a99951d5834a8b8b96ddf4e2d582ee9dd61864dae"
|
|
recipient_address = "1LK4JDfxaYZjJAinao3q5KdrLCtW3AFeQ6"
|
|
current_block_number = 373610
|
|
prior_block_number = 373601
|
|
txid = "ce99f01aa17995c77e041beee93cf3bcf47ef68d18c16f79edd120a204d7c808"
|
|
vtxindex = 1
|
|
op = NAME_REGISTRATION
|
|
opcode = "NAME_REGISTRATION"
|
|
op_fee = 640000
|
|
consensus_hash = "54a451b8a09a2acd951b06bda2b8e69f"
|
|
|
|
namespace_address = "12HcV1f7XtQTgSPt7r1mpyr1ppfnX8fPa4"
|
|
namespace_base = 4
|
|
namespace_block_number = 373500
|
|
namespace_buckets = [6,5,4,3,2,1,0,0,0,0,0,0,0,0,0,0]
|
|
namespace_coeff = 250
|
|
namespace_lifetime = 520000
|
|
namespace_id = "test"
|
|
namespace_id_hash = "6134faee8737a865995aa5423b55f1a8ec69fe4b"
|
|
namespace_no_vowel_discount = 10
|
|
namespace_nonalpha_discount = 10
|
|
namespace_ready_block = 373600
|
|
namespace_recipient = "76a914b7e40511f53f69045cb14c6c5a714d6a4ffe3a3788ac"
|
|
namespace_recipient_address = "1HmKpCXiK4ExFbdTG1Y38jcrtx9KPkgYKX"
|
|
namespace_reveal_block = 373510
|
|
namespace_sender = "76a914b7e40511f53f69045cb14c6c5a714d6a4ffe3a3788ac"
|
|
namespace_sender_pubkey = "04d6fd1ba0effaf1e8d94ea7b7a3d0ef26fea00a14ce5ffcc1495fe588a2c6d0f35eaa22c01a35bee5817f03d769a3a38a3bb50182c61449ad125555daf26396fb"
|
|
namespace_txid = "71754175ee3168ade90b74c78af58312708a7103fd2d8d17346cad7a49b934da"
|
|
namespace_version = 1
|
|
|
|
namespace_record = {
|
|
"address": namespace_address,
|
|
"base": namespace_base,
|
|
"block_number": namespace_reveal_block,
|
|
"buckets": namespace_buckets,
|
|
"coeff": namespace_coeff,
|
|
"lifetime": namespace_lifetime,
|
|
"namespace_id": namespace_id,
|
|
"no_vowel_discount": namespace_no_vowel_discount,
|
|
"nonalpha_discount": namespace_nonalpha_discount,
|
|
"preorder_hash": namespace_id_hash,
|
|
"ready_block": namespace_ready_block,
|
|
# "opcode": "NAMESPACE_READY",
|
|
"op": NAMESPACE_REVEAL,
|
|
"op_fee": 6140,
|
|
"recipient": namespace_recipient,
|
|
"recipient_address": namespace_recipient_address,
|
|
"reveal_block": namespace_reveal_block,
|
|
"sender": namespace_sender,
|
|
"sender_pubkey": namespace_sender_pubkey,
|
|
"txid": namespace_txid,
|
|
"vtxindex": 3,
|
|
"version": namespace_version
|
|
}
|
|
|
|
preorder_record = {
|
|
# NOTE: was preorder_name_hash
|
|
"preorder_hash": hash_name( name, sender, recipient_address ),
|
|
"consensus_hash": consensus_hash,
|
|
"block_number": prior_block_number,
|
|
"sender": sender,
|
|
"sender_pubkey": sender_pubkey,
|
|
"address": "1Nrmkp6rhebJnL2wURkUrwH93Evaq1s3Yd",
|
|
"fee": 12345,
|
|
"op": NAME_PREORDER,
|
|
"txid": "69c21d76a98dd450305200346602d38c2ee2c401a81acd3dbe9ead850ab6bc7b",
|
|
"vtxindex": 20,
|
|
"op_fee": 6400001
|
|
}
|
|
|
|
name_record = {
|
|
'name': name,
|
|
"preorder_hash": hash_name( name, sender, recipient_address ),
|
|
'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': str(recipient_pubkey),
|
|
'address': str(recipient_address),
|
|
|
|
'block_number': prior_block_number,
|
|
'preorder_block_number': preorder_record['block_number'],
|
|
'first_registered': current_block_number,
|
|
'last_renewed': current_block_number,
|
|
'revoked': False,
|
|
|
|
'op': op,
|
|
'txid': txid,
|
|
'vtxindex': vtxindex,
|
|
'op_fee': op_fee,
|
|
|
|
# (not imported)
|
|
'importer': None,
|
|
'importer_address': None,
|
|
'consensus_hash': preorder_record['consensus_hash']
|
|
}
|
|
|
|
namespace_preorder_record = {
|
|
"address": pybitcoin.script_hex_to_address( namespace_record['sender'] ),
|
|
"block_number": 373400,
|
|
"consensus_hash": "1c54465d3486f07be2c7a81af0ef44ad",
|
|
"fee": 6160,
|
|
"preorder_hash": namespace_id_hash,
|
|
"op": NAMESPACE_PREORDER,
|
|
"op_fee": 40000000,
|
|
"sender": namespace_record['sender'],
|
|
"sender_pubkey": namespace_record['sender_pubkey'],
|
|
"txid": namespace_record['namespace_id'],
|
|
"vtxindex": 3
|
|
}
|
|
|
|
name_update_op = {
|
|
'op': NAME_UPDATE,
|
|
'op_fee': 6140,
|
|
'vtxindex': 4,
|
|
'txid': '0e0a3ed6145b6424267ae042911fbfc69e21a17d8c579ac9b114ba934ccda950',
|
|
'block_number': 373701,
|
|
'consensus_hash': '4017d71d6c5e87c9efe8633f1dc1c425',
|
|
'name_hash': hash256_trunc128( name_record['name'] + '4017d71d6c5e87c9efe8633f1dc1c425' ),
|
|
'value_hash': '11' * 20,
|
|
}
|
|
|
|
cur = db.cursor()
|
|
print "namespace preorder"
|
|
namedb_preorder_insert( cur, namespace_preorder_record )
|
|
db.commit()
|
|
|
|
print "namespace reveal"
|
|
ns_preorder_rec = namedb_get_preorder( cur, namespace_preorder_record['preorder_hash'], namespace_preorder_record['block_number'] )
|
|
namedb_state_create( cur, "NAMESPACE_REVEAL", namespace_record, namespace_record['reveal_block'], namespace_record['vtxindex'], namespace_record['txid'], namespace_record['namespace_id'], ns_preorder_rec, "namespaces" )
|
|
db.commit()
|
|
|
|
print "name preorder"
|
|
namedb_preorder_insert( cur, preorder_record )
|
|
db.commit()
|
|
|
|
print "name register"
|
|
nm_preorder_rec = namedb_get_preorder( cur, preorder_record['preorder_hash'], preorder_record['block_number'] )
|
|
print "preorder:\n%s\n" % json.dumps(nm_preorder_rec, indent=4)
|
|
namedb_state_create( cur, "NAME_REGISTRATION", name_record, name_record['block_number'], name_record['vtxindex'], name_record['txid'], name_record['name'], nm_preorder_rec, 'name_records' )
|
|
db.commit()
|
|
|
|
print "name update"
|
|
nm_rec = namedb_get_name( cur, name_record['name'], current_block=name_record['block_number'] )
|
|
print "register:\n%s\n" % json.dumps(nm_rec, indent=4)
|
|
namedb_state_transition( cur, "NAME_UPDATE", name_update_op, name_update_op['block_number'], name_update_op['vtxindex'], name_update_op['txid'], name_record['name'], name_record, 'name_records' )
|
|
db.commit()
|
|
|
|
nm_rec = namedb_get_name( cur, name_record['name'], current_block=name_record['block_number'] )
|
|
print "after update:\n%s\n" % json.dumps(nm_rec, indent=4)
|
|
|
|
print "\nhistory:\n"
|
|
hist = namedb_get_history( cur, name )
|
|
print json.dumps( hist, indent=4 )
|
|
|
|
db.close()
|
|
|