Files
stacks-puppet-node/blockstack/blockstackd.py
Jude Nelson 2a9f35520d WIP: treat name operations like true state transitions, and handle
transaction-creation and broadcasting at the API-level
2016-06-06 15:21:38 -04:00

2874 lines
99 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 argparse
import logging
import os
import sys
import subprocess
import signal
import json
import datetime
import traceback
import httplib
import time
import socket
import math
import random
import shutil
import tempfile
import binascii
import copy
import atexit
import threading
import errno
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
# stop common XML attacks
from defusedxml import xmlrpc
xmlrpc.monkey_patch()
import virtualchain
log = virtualchain.get_logger("blockstack-server")
try:
import blockstack_client
except:
# storage API won't work
blockstack_client = None
from ConfigParser import SafeConfigParser
from lib import nameset as blockstack_state_engine
from lib import get_db_state
from lib.config import REINDEX_FREQUENCY, DEFAULT_DUST_FEE, NAMESPACE_ID_BLOCKCHAIN
from lib import *
from lib.storage import *
import lib.nameset.virtualchain_hooks as virtualchain_hooks
import lib.config as config
# global variables, for use with the RPC server and the twisted callback
rpc_server = None
def get_pidfile_path():
"""
Get the PID file path.
"""
working_dir = virtualchain.get_working_dir()
pid_filename = blockstack_state_engine.get_virtual_chain_name() + ".pid"
return os.path.join( working_dir, pid_filename )
def put_pidfile( pidfile_path, pid ):
"""
Put a PID into a pidfile
"""
with open( pidfile_path, "w" ) as f:
f.write("%s" % pid)
return
def get_logfile_path():
"""
Get the logfile path for our service endpoint.
"""
working_dir = virtualchain.get_working_dir()
logfile_filename = blockstack_state_engine.get_virtual_chain_name() + ".log"
return os.path.join( working_dir, logfile_filename )
def get_state_engine():
"""
Get a handle to the blockstack virtual chain state engine.
"""
return get_db_state()
def get_lastblock():
"""
Get the last block processed.
"""
lastblock_filename = virtualchain.get_lastblock_filename()
if not os.path.exists( lastblock_filename ):
return None
try:
with open(lastblock_filename, "r") as f:
lastblock_txt = f.read()
lastblock = int(lastblock_txt.strip())
return lastblock
except:
return None
def get_index_range(blockchain_name):
"""
Get the block index range for the given blockchain.
Mask connection failures with timeouts.
Always try to reconnect.
The last block will be the last block to search for names.
This will be NUM_CONFIRMATIONS behind the actual last-block the
cryptocurrency node knows about.
"""
num_confirmations = virtualchain.blockchain_confirmations(blockchain_name)
blockchain_client = get_blockchain_client( blockchain_name, new=True )
first_block = None
last_block = None
while last_block is None:
first_block, last_block = virtualchain.get_index_range( blockchain_client )
if last_block is None:
# try to reconnnect
time.sleep(1)
log.error("Reconnect to blockchain '%s'" % blockchain_name)
blockchain_client = get_blockchain_client( blockchain_name, new=True )
continue
else:
return first_block, last_block - num_confirmations
def rpc_traceback():
exception_data = traceback.format_exc().splitlines()
return {
"error": exception_data[-1],
"traceback": exception_data
}
def get_name_cost( name ):
"""
Get the cost of a name, given the fully-qualified name.
Do so by finding the namespace it belongs to (even if the namespace is being imported).
Return None if the namespace has not been declared
"""
db = get_state_engine()
namespace_id = get_namespace_from_name( name )
if namespace_id is None or len(namespace_id) == 0:
return None
namespace = db.get_namespace( namespace_id )
if namespace is None:
# maybe importing?
namespace = db.get_namespace_reveal( namespace_id )
if namespace is None:
# no such namespace
return None
name_fee = price_name( get_name_from_fq_name( name ), namespace )
return name_fee
def get_max_subsidy( testset=False ):
"""
Get the maximum subsidy we offer, and get a key with a suitable balance
to pay the subsidy.
Return (subsidy, key)
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
if blockstack_opts.get("max_subsidy") is None:
return (None, None)
return blockstack_opts["max_subsidy"]
def make_subsidized_tx( blockchain_name, unsigned_tx, fee_cb, max_subsidy, subsidy_key ):
"""
Create a subsidized transaction
transaction and a callback that determines the fee structure.
"""
# subsidize the transaction
subsidized_tx = tx_make_subsidizable( unsigned_tx, fee_cb, max_subsidy, subsidy_key, blockchain_client_inst )
if subsidized_tx is None:
return {"error": "Order exceeds maximum subsidy"}
else:
resp = {
"subsidized_tx": subsidized_tx
}
return resp
def blockstack_name_preorder( name, privatekey, register_addr, tx_only=False, subsidy_key=None, consensus_hash=None ):
"""
Preorder a name.
@name: the name to preorder
@register_addr: the address that will own the name upon registration
@privatekey: the private key that will pay for the preorder. Can be None if we're subsidizing (in which case subsidy_key is required)
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then return a subsidized serialized transaction, where we have signed our
inputs/outputs with SIGHASH_ANYONECANPAY. The caller will need to sign their input and then
broadcast it.
@subsidy_key: if given, then this transaction will be subsidized with this key and returned (but not broadcasted)
This forcibly sets tx_only=True and pay_fee=False.
Return a JSON object on success.
Return a JSON object with 'error' set on error.
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockchain_name = namespace_to_blockchain( get_namespace_from_name( name ) )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
db = get_state_engine()
if consensus_hash is None:
consensus_hash = db.get_current_consensus()
if consensus_hash is None:
# consensus hash must exist
return {"error": "Nameset snapshot not found."}
if db.is_name_registered( name ):
# name can't be registered
return {"error": "Name already registered"}
if not db.is_namespace_ready( namespace_id ):
# namespace must be ready; otherwise this is a waste
return {"error": "Namespace is not ready"}
name_fee = get_name_cost( name )
log.debug("The price of '%s' is %s satoshis" % (name, name_fee))
if privatekey is not None:
privatekey = str(privatekey)
public_key = None
if subsidy_key is not None:
subsidy_key = str(subsidy_key)
tx_only = True
# the sender will be the subsidizer (otherwise it will be the given private key's owner)
public_key = ECPrivateKey( subsidy_key ).public_key().to_hex()
resp = {}
inputs = []
outputs = []
try:
"""
resp = preorder_name(str(name), privatekey, str(register_addr), str(consensus_hash), blockchain_client_inst, \
name_fee, blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'], subsidy_public_key=public_key, tx_only=tx_only )
"""
inputs, outputs = preorder_state_transition( str(name), privatekey, str(register_addr), str(consensus_hash), name_fee, subsidy_public_key=public_key )
except:
return rpc_traceback()
if subsidy_key is not None:
# make subsidized
tx_signed = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_signed
}
elif not tx_only:
# send transaction
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
else:
# give back transaction
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
'unsigned_tx': tx_unsigned
}
log.debug('preorder <name, consensus_hash>: <%s, %s>' % (name, consensus_hash))
return resp
def blockstack_name_register( name, privatekey, register_addr, renewal_fee=None, tx_only=False, subsidy_key=None, user_public_key=None, testset=False, consensus_hash=None ):
"""
Register or renew a name
@name: the name to register
@register_addr: the address that will own the name (must be the same as the address
given on preorder)
@privatekey: if registering, this is the key that will pay for the registration (must
be the same key as the key used to preorder). If renewing, this is the private key of the
name owner's address.
@renewal_fee: if given, this is the fee to renew the name (must be at least the
cost of the name itself)
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then do not pay any associated dust or operational fees. This should be used
to generate a signed serialized transaction that another key will later subsidize
Return a JSON object on success
Return a JSON object with 'error' set on error.
"""
namespace_id = get_namespace_from_name( name )
blockchain_name = namespace_to_blockchain( namespace_id )
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
db = get_state_engine()
if db.is_name_registered( name ) and renewal_fee is None:
# *must* be given, so we don't accidentally charge
return {"error": "Name already registered"}
public_key = None
if subsidy_key is not None:
subsidy_key = str(subsidy_key)
tx_only = True
# the sender will be the subsidizer (otherwise it will be the given private key's owner)
public_key = ECPrivateKey( subsidy_key ).public_key().to_hex()
inputs = []
outputs = []
resp = {}
try:
"""
resp = register_name(str(name), privatekey, str(register_addr), blockchain_client_inst, renewal_fee=renewal_fee, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'], \
subsidy_public_key=public_key, user_public_key=user_public_key )
"""
inputs, outputs = register_state_transition( str(name), privatekey, str(register_ddr), renewal_fee=renewal_fee, subsidy_public_key=public_key, user_public_key=user_public_key )
except:
return rpc_traceback()
if subsidy_key is not None and renewal_fee is not None:
inputs, outputs = subsidize_state_transition( blockchain_name, inputs, outputs, registration_fees, blockstack_opts['max_subsidy'], subsidy_key )
if subsidy_key is None:
if tx_only:
# only want unsigned tx
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
"unsigned_tx": tx_unsigned
}
else:
# send it off!
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
else:
tx_subsidized = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_subsidized
}
log.debug("name register/renew: %s" % name)
return resp
def blockstack_name_update( name, data_hash, privatekey, tx_only=False, user_public_key=None, subsidy_key=None, testset=False, consensus_hash=None ):
"""
Update a name with new data.
@name: the name to update
@data_hash: the hash of the new name record
@privatekey: the private key of the owning address.
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then do not pay any associated dust or operational fees. This should be
used to generate a signed serialized transaction that another key will later subsidize.
Return a JSON object on success
Return a JSON object with 'error' set on error.
"""
namespace_id = get_namespace_from_name( name )
blockchain_name = namespace_to_blockchain( namespace_id )
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
db = get_state_engine()
if consensus_hash is None:
consensus_hash = db.get_current_consensus()
if consensus_hash is None:
return {"error": "Nameset snapshot not found."}
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
resp = {}
inputs = []
outputs = []
try:
"""
resp = update_name(str(name), str(data_hash), str(consensus_hash), privatekey, blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, user_public_key=user_public_key, testset=blockstack_opts['testset'])
"""
inputs, outputs = update_state_transition( str(name), str(data_hash), str(consensus_hash), privatekey, user_public_key=user_public_key )
except:
return rpc_traceback()
if subsidy_key is None:
if tx_only:
# only want unsigned tx
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
"unsigned_tx": tx_unsigned
}
else:
# send it off!
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
else:
inputs, outputs = subsidize_state_transition( blockchain_name, inputs, outputs, update_fees, blockstack_opts['max_subsidy'], subsidy_key )
tx_subsidized = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_subsidized
}
log.debug('name update <name, data_hash, consensus_hash>: <%s, %s, %s>' % (name, data_hash, consensus_hash))
return resp
def blockstack_name_transfer( name, address, keepdata, privatekey, user_public_key=None, subsidy_key=None, tx_only=False, testset=False, consensus_hash=None ):
"""
Transfer a name to a new address.
@name: the name to transfer
@address: the new address to own the name
@keepdata: if True, then keep the name record tied to the name. Otherwise, discard it.
@privatekey: the private key of the owning address.
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then do not pay any associated dust or operational fees. This should be
used to generate a signed serialized transaction that another key will later subsidize.
Return a JSON object on success
Return a JSON object with 'error' set on error.
"""
namespace_id = get_namespace_from_name( name )
blockchain_name = namespace_to_blockchain( namespace_id )
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
db = get_state_engine()
if consensus_hash is None:
consensus_hash = db.get_current_consensus()
if consensus_hash is None:
return {"error": "Nameset snapshot not found."}
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
if type(keepdata) != bool:
if str(keepdata) == "True":
keepdata = True
else:
keepdata = False
resp = {}
inputs = []
outputs = []
try:
"""
resp = transfer_name(str(name), str(address), keepdata, str(consensus_hash), privatekey, blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, user_public_key=user_public_key, testset=blockstack_opts['testset'])
"""
inputs, outputs = transfer_state_transition( str(name), str(address), keepdata, str(consensus_hash), private_key, user_public_key=user_public_key )
except:
return rpc_traceback()
if subsidy_key is None:
if tx_only:
# only want unsigned tx
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
"unsigned_tx": tx_unsigned
}
else:
# send it off!
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
else:
inputs, outputs = subsidize_state_transition( blockchain_name, inputs, outputs, transfer_fees, blockstack_opts['max_subsidy'], subsidy_key )
tx_subsidized = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_subsidized
}
log.debug('name transfer <name, address>: <%s, %s>' % (name, address))
return resp
def blockstack_name_renew( name, privatekey, register_addr=None, tx_only=False, subsidy_key=None, user_public_key=None, testset=False, consensus_hash=None ):
"""
Renew a name
@name: the name to renew
@privatekey: the private key of the name owner
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then do not pay any associated dust or operational fees. This should be
used to generate a signed serialized transaction that another key will later subsidize.
Return a JSON object on success
Return a JSON object with 'error' set on error.
"""
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
# renew the name for the caller
db = get_state_engine()
name_rec = db.get_name( name )
if name_rec is None:
return {"error": "Name is not registered"}
# renew to the caller (should be the same as the sender)
if register_addr is None:
register_addr = name_rec['address']
if str(register_addr) != str(ECPrivateKey( privatekey ).public_key().address()):
return {"error": "Only the name's owner can send a renew request"}
renewal_fee = get_name_cost( name )
return blockstack_name_register( name, privatekey, register_addr, renewal_fee=renewal_fee, tx_only=tx_only, subsidy_key=subsidy_key, user_public_key=user_public_key, testset=testset )
def blockstack_name_revoke( name, privatekey, tx_only=False, subsidy_key=None, user_public_key=None, testset=False, consensus_hash=None ):
"""
Revoke a name and all its data.
@name: the name to renew
@privatekey: the private key of the name owner
@tx_only: if True, then return only the unsigned serialized transaction. Do not broadcast it.
@pay_fee: if False, then do not pay any associated dust or operational fees. This should be
used to generate a signed serialized transaction that another key will later subsidize.
Return a JSON object on success
Return a JSON object with 'error' set on error.
"""
namespace_id = get_namespace_from_name( name )
blockchain_name = namespace_to_blockchain( namespace_id )
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain service provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
resp = {}
inputs = []
outputs = []
try:
"""
resp = revoke_name(str(name), privatekey, blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, \
user_public_key=user_public_key, testset=blockstack_opts['testset'])
"""
inputs, outputs = revoke_state_transition( str(name), user_public_key=user_public_key )
except:
return rpc_traceback()
if subsidy_key is None:
if tx_only:
# only want unsigned tx
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
"unsigned_tx": tx_unsigned
}
else:
# send it off!
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
else:
inputs, outputs = subsidize_state_transition( blockchain_name, inputs, outputs, revoke_fees, blockstack_opts['max_subsidy'], subsidy_key )
tx_subsidized = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_subsidized
}
log.debug("name revoke <%s>" % name )
return resp
def blockstack_name_import( name, recipient_address, update_hash, privatekey, tx_only=False, testset=False, consensus_hash=None ):
"""
Import a name into a namespace.
"""
namespace_id = get_namespace_from_name( name )
blockchain_name = namespace_to_blockchain( namespace_id )
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(blockchain_name)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain service provider"}
broadcaster_client_inst = tx_broadcast_service_connect(blockchain_name)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
db = get_state_engine()
resp = {}
inputs = []
outputs = []
try:
"""
resp = name_import( str(name), str(recipient_address), str(update_hash), str(privatekey), blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'] )
"""
inputs, outputs = name_import_state_transition( str(name), str(recipient_address), str(update_hash), privatekey )
except:
return rpc_traceback()
if tx_only:
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
'unsigned_tx': tx_unsigned
}
else:
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
log.debug("import <%s>" % name )
return resp
def blockstack_namespace_preorder( namespace_id, register_addr, privatekey, tx_only=False, testset=False, consensus_hash=None ):
"""
Define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockchain_name = namespace_to_blockchain( namespace_id )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
db = get_state_engine()
"""
blockchain_client_inst = blockchain_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
if consensus_hash is None:
consensus_hash = db.get_current_consensus()
if consensus_hash is None:
return {"error": "Nameset snapshot not found."}
namespace_fee = price_namespace( namespace_id )
log.debug("Namespace '%s' will cost %s satoshis" % (namespace_id, namespace_fee))
resp = {}
inputs = []
outputs = []
try:
"""
resp = namespace_preorder( str(namespace_id), str(register_addr), str(consensus_hash), str(privatekey), blockchain_client_inst, namespace_fee, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'] )
"""
inputs, outputs = namespace_preorder_state_transition( str(namespace_id), str(register_addr), str(consensus_hash), privatekey, namespace_fee )
except:
return rpc_traceback()
if tx_only:
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
'unsigned_tx': tx_unsigned
}
else:
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
log.debug("namespace_preorder <%s>" % (namespace_id))
return resp
def blockstack_namespace_reveal( namespace_id, register_addr, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount, privatekey, tx_only=False, testset=False, consensus_hash=None ):
"""
Reveal and define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockchain_name = namespace_to_blockchain(namespace_id)
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
resp = {}
inputs = []
outputs = []
try:
"""
resp = namespace_reveal( str(namespace_id), str(register_addr), int(lifetime), \
int(coeff), int(base), list(bucket_exponents), \
int(nonalpha_discount), int(no_vowel_discount), \
str(privatekey), blockchain_client_inst, \
blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'], tx_only=tx_only )
"""
inputs, outputs = namespace_reveal_state_transition( str(namespace_id), str(register_addr), int(lifetime), int(coeff), int(base), \
list(bucket_exponents), int(nonalpha_discount), int(no_vowel_discount), \
privatekey )
except:
return rpc_traceback()
if tx_only:
tx_unsigned = make_transaction(blockchain_name, inputs, outputs, None)
resp = {
'unsigned_tx': tx_unsigned
}
else:
resp = send_transaction(blockchain_name, inputs, outputs, privatekey)
log.debug("namespace_reveal <%s, %s, %s, %s, %s, %s, %s>" % (namespace_id, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount))
return resp
def blockstack_namespace_ready( namespace_id, privatekey, tx_only=False, testset=False, consensus_hash=None ):
"""
Declare that a namespace is open to accepting new names.
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockchain_name = namespace_to_blockchain(namespace_id)
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
resp = {}
inputs = []
outputs = []
try:
"""
resp = namespace_ready( str(namespace_id), str(privatekey), blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, testset=blockstack_opts['testset'] )
"""
inputs, outputs = namespace_ready_state_transition( str(namespace_id), privatekey )
except:
return rpc_traceback()
if tx_only:
tx_unsigned = make_transaction(blockchain_name, inputs, outputs, None)
resp = {
'unsigned_tx': tx_unsigned
}
else:
resp = send_transaction(blockchain_name, inputs, outputs, privatekey)
log.debug("namespace_ready %s" % namespace_id )
return resp
def blockstack_announce( message, privatekey, tx_only=False, subsidy_key=None, user_public_key=None, testset=False ):
"""
Send an announcement via the blockchain.
If we're sending the tx out, then also replicate the message text to storage providers, via the blockstack_client library
"""
# blockstack_opts = default_blockstack_opts( virtualchain.get_config_filename(), testset=testset )
blockstack_opts = get_blockstack_opts()
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
"""
blockchain_client_inst = blockchain_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if blockchain_client_inst is None:
return {"error": "Failed to connect to blockchain UTXO provider"}
broadcaster_client_inst = tx_broadcast_service_connect(NAMESPACE_ID_BLOCKCHAIN)
if broadcaster_client_inst is None:
return {"error": "Failed to connect to blockchain transaction broadcaster"}
"""
message_hash = virtualchain.hex_hash160( message )
resp = {}
try:
"""
resp = send_announce( message_hash, privatekey, blockchain_client_inst, \
tx_only=tx_only, blockchain_broadcaster=broadcaster_client_inst, \
user_public_key=user_public_key, testset=blockstack_opts['testset'])
"""
inputs, outputs = announce_state_transition( message_hash, privatekey )
except:
return rpc_traceback()
if subsidy_key is None:
if tx_only:
# only want unsigned tx
tx_unsigned = make_transaction( blockchain_name, inputs, outputs, None )
resp = {
"unsigned_tx": tx_unsigned
}
else:
# send it off!
resp = send_transaction( blockchain_name, inputs, outputs, privatekey )
if 'error' not in resp:
# success!
data_hash = put_announcement( message, resp['transaction_hash'] )
if data_hash is None:
resp['error'] = 'Failed to store message text'
else:
resp['data_hash'] = data_hash
else:
inputs, outputs = subsidize_state_transition( blockchain_name, inputs, outputs, announce_fees, blockstack_opts['max_subsidy'], subsidy_key )
tx_subsidized = make_transaction( blockchain_name, inputs, outputs, subsidy_key )
resp = {
'subsidized_tx': tx_subsidized
}
log.debug("announce <%s>" % message_hash )
return resp
class BlockstackdRPCHandler(SimpleXMLRPCRequestHandler):
"""
Hander to capture tracebacks
"""
def _dispatch(self, method, params):
try:
res = self.server.funcs["rpc_" + str(method)](*params)
# lol jsonrpc within xmlrpc
return json.dumps(res)
except Exception, e:
print >> sys.stderr, "\n\n%s\n\n" % traceback.format_exc()
return rpc_traceback()
class BlockstackdRPC(SimpleXMLRPCServer):
"""
Blockstackd RPC server, used for querying
the name database and the blockchain peer.
Methods that start with rpc_* will be registered
as RPC methods.
"""
def __init__(self, host='0.0.0.0', port=config.RPC_SERVER_PORT, handler=BlockstackdRPCHandler, testset=False):
self.testset = testset
log.info("Listening on %s:%s" % (host, port))
SimpleXMLRPCServer.__init__( self, (host, port), handler, allow_none=True )
# register methods
for attr in dir(self):
if attr.startswith("rpc_"):
method = getattr(self, attr)
if callable(method) or hasattr(method, '__call__'):
self.register_function( method )
def rpc_ping(self):
reply = {}
reply['status'] = "alive"
return reply
def rpc_get_name_blockchain_record(self, name):
"""
Lookup the blockchain-derived profile for a name.
"""
db = get_state_engine()
try:
name = str(name)
except Exception as e:
return {"error": str(e)}
name_record = db.get_name(str(name))
if name_record is None:
if is_indexing():
return {"error": "Indexing blockchain"}
else:
return {"error": "Not found."}
else:
return name_record
def rpc_get_name_blockchain_history( self, name, start_block, end_block ):
"""
Get the sequence of name operations processed for a given name.
"""
db = get_state_engine()
name_history = db.get_name_history( name, start_block, end_block )
if name_history is None:
if is_indexing():
return {"error": "Indexing blockchain"}
else:
return {"error": "Not found."}
else:
return name_history
def rpc_get_nameops_at( self, block_id ):
"""
Get the sequence of names and namespaces altered at the given block.
Returns the list of name operations to be fed into virtualchain.
Used by SNV clients.
"""
db = get_state_engine()
all_ops = db.get_all_nameops_at( block_id )
ret = []
for op in all_ops:
restored_op = nameop_restore_consensus_fields( op, block_id )
ret.append( restored_op )
return ret
def rpc_get_nameops_hash_at( self, block_id ):
"""
Get the hash over the sequence of names and namespaces altered at the given block.
Used by SNV clients.
"""
db = get_state_engine()
ops = db.get_all_nameops_at( block_id )
if ops is None:
ops = []
restored_ops = []
for op in ops:
restored_op = nameop_restore_consensus_fields( op, block_id )
restored_ops.append( restored_op )
# NOTE: extracts only the operation-given fields, and ignores ancilliary record fields
serialized_ops = [ virtualchain.StateEngine.serialize_op( str(op['op'][0]), op, BlockstackDB.make_opfields(), verbose=False ) for op in restored_ops ]
for serialized_op in serialized_ops:
log.debug("SERIALIZED (%s): %s" % (block_id, serialized_op))
ops_hash = virtualchain.StateEngine.make_ops_snapshot( serialized_ops )
log.debug("Serialized hash at (%s): %s" % (block_id, ops_hash))
return ops_hash
def rpc_getinfo(self):
"""
Get the number of blocks the
"""
bitcoin_client = get_blockchain_client( "bitcoin", new=True )
info = bitcoin_client.getinfo()
reply = {}
reply['bitcoind_blocks'] = info['blocks'] # legacy
reply['blockchain_blocks'] = info['blocks']
db = get_state_engine()
reply['consensus'] = db.get_current_consensus()
reply['blocks'] = db.get_current_block()
reply['blockstack_version'] = "%s" % VERSION
reply['testset'] = str(self.testset)
reply['last_block'] = reply['blocks']
return reply
def rpc_get_names_owned_by_address(self, address):
"""
Get the list of names owned by an address.
Valid only for names with p2pkh sender scripts.
"""
db = get_state_engine()
names = db.get_names_owned_by_address( address )
if names is None:
names = []
return names
def rpc_preorder( self, name, privatekey, register_addr ):
"""
Preorder a name:
@name is the name to preorder
@register_addr is the address of the key pair that will own the name
@privatekey is the private key that will send the preorder transaction
(it must be *different* from the register_addr keypair)
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_name_preorder( str(name), str(privatekey), str(register_addr), testset=self.testset )
def rpc_preorder_tx( self, name, privatekey, register_addr ):
"""
Generate a transaction that preorders a name:
@name is the name to preorder
@register_addr is the address of the key pair that will own the name
@privatekey is the private key that will send the preorder transaction
(it must be *different* from the register_addr keypair)
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_preorder( str(name), str(privatekey), str(register_addr), tx_only=True, testset=self.testset )
def rpc_preorder_tx_subsidized( self, name, register_addr, subsidy_key ):
"""
Generate a transaction that preorders a name, but without paying fees.
@name is the name to preorder
@register_addr is the address of the key pair that will own the name
(it must be *different* from the register_addr keypair)
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_preorder( str(name), None, str(register_addr), tx_only=True, subsidy_key=str(subsidy_key), testset=self.testset )
def rpc_register( self, name, privatekey, register_addr ):
"""
Register a name:
@name is the name to register
@register_addr is the address of the key pair that will own the name
(given earlier in the preorder)
@privatekey is the private key that sent the preorder transaction.
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_name_register( str(name), str(privatekey), str(register_addr), testset=self.testset )
def rpc_register_tx( self, name, privatekey, register_addr ):
"""
Generate a transaction that will register a name:
@name is the name to register
@register_addr is the address of the key pair that will own the name
(given earlier in the preorder)
@privatekey is the private key that sent the preorder transaction.
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_register( str(name), str(privatekey), str(register_addr), tx_only=True, testset=self.testset )
def rpc_register_tx_subsidized( self, name, user_public_key, register_addr, subsidy_key ):
"""
Generate a subsidizable transaction that will register a name
@name is the name to register
@register_addr is the address of the key pair that will own the name
(given earlier in the preorder)
@user_public_key is the public key whose private counterpart sent the preorder transaction.
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_register( str(name), None, str(register_addr), tx_only=True, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), testset=self.testset )
def rpc_update( self, name, data_hash, privatekey ):
"""
Update a name's record:
@name is the name to update
@data_hash is the hash of the new name record
@privatekey is the private key that owns the name
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_name_update( str(name), str(data_hash), str(privatekey), testset=self.testset )
def rpc_update_tx( self, name, data_hash, privatekey ):
"""
Generate a transaction that will update a name's name record hash.
@name is the name to update
@data_hash is the hash of the new name record
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_update( str(name), str(data_hash), str(privatekey), tx_only=True, testset=self.testset )
def rpc_update_tx_subsidized( self, name, data_hash, user_public_key, subsidy_key ):
"""
Generate a subsidizable transaction that will update a name's name record hash.
@name is the name to update
@data_hash is the hash of the new name record
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_update( str(name), str(data_hash), None, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), tx_only=True, testset=self.testset )
def rpc_transfer( self, name, address, keepdata, privatekey ):
"""
Transfer a name's record to a new address
@name is the name to transfer
@address is the new address that will own the name
@keepdata determines whether or not the name record will
remain associated with the name on transfer.
@privatekey is the private key that owns the name
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
# coerce boolean
if type(keepdata) != bool:
if str(keepdata) == "True":
keepdata = True
else:
keepdata = False
return blockstack_name_transfer( str(name), str(address), keepdata, str(privatekey), testset=self.testset )
def rpc_transfer_tx( self, name, address, keepdata, privatekey ):
"""
Generate a transaction that will transfer a name to a new address
@name is the name to transfer
@address is the new address that will own the name
@keepdata determines whether or not the name record will
remain associated with the name on transfer.
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
# coerce boolean
if type(keepdata) != bool:
if str(keepdata) == "True":
keepdata = True
else:
keepdata = False
return blockstack_name_transfer( str(name), str(address), keepdata, str(privatekey), tx_only=True, testset=self.testset )
def rpc_transfer_tx_subsidized( self, name, address, keepdata, user_public_key, subsidy_key ):
"""
Generate a subsidizable transaction that will transfer a name to a new address
@name is the name to transfer
@address is the new address that will own the name
@keepdata determines whether or not the name record will
remain associated with the name on transfer.
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
# coerce boolean
if type(keepdata) != bool:
if str(keepdata) == "True":
keepdata = True
else:
keepdata = False
return blockstack_name_transfer( str(name), str(address), keepdata, None, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), tx_only=True, testset=self.testset )
def rpc_renew( self, name, privatekey ):
"""
Renew a name:
@name is the name to renew
@privatekey is the private key that owns the name
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_name_renew( str(name), str(privatekey), testset=self.testset )
def rpc_renew_tx( self, name, privatekey ):
"""
Generate a transaction that will register a name:
@name is the name to renew
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_renew( str(name), str(privatekey), tx_only=True, testset=self.testset )
def rpc_renew_tx_subsidized( self, name, user_public_key, subsidy_key ):
"""
Generate a subsidizable transaction that will register a name
@name is the name to renew
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_renew( name, None, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), tx_only=True, testset=self.testset )
def rpc_revoke( self, name, privatekey ):
"""
revoke a name:
@name is the name to revoke
@privatekey is the private key that owns the name
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_name_revoke( str(name), str(privatekey), testset=self.testset )
def rpc_revoke_tx( self, name, privatekey ):
"""
Generate a transaction that will revoke a name:
@name is the name to revoke
@privatekey is the private key that owns the name
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_revoke( str(name), str(privatekey), tx_only=True, testset=self.testset )
def rpc_revoke_tx_subsidized( self, name, user_public_key, subsidy_key ):
"""
Generate a subsidizable transaction that will revoke a name
@name is the name to revoke
@privatekey is the private key that owns the name
@user_public_key is the public key of the name owner. Must be given if @subsidy_key is given.
@subsidy_key is the key that will pay for the tx
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_name_revoke( str(name), None, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), tx_only=True, testset=self.testset )
def rpc_name_import( self, name, recipient_address, update_hash, privatekey ):
"""
Import a name into a namespace.
"""
return blockstack_name_import( name, recipient_address, update_hash, privatekey, testset=self.testset )
def rpc_name_import_tx( self, name, recipient_address, update_hash, privatekey ):
"""
Generate a tx that will import a name
"""
return blockstack_name_import( name, recipient_address, update_hash, privatekey, tx_only=True, testset=self.testset )
def rpc_namespace_preorder( self, namespace_id, reveal_addr, privatekey ):
"""
Define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
return blockstack_namespace_preorder( namespace_id, reveal_addr, privatekey, testset=self.testset )
def rpc_namespace_preorder_tx( self, namespace_id, reveal_addr, privatekey ):
"""
Create a signed transaction that will define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
return blockstack_namespace_preorder( namespace_id, reveal_addr, privatekey, tx_only=True, testset=self.testset )
def rpc_namespace_reveal( self, namespace_id, reveal_addr, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount, privatekey ):
"""
Reveal and define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
return blockstack_namespace_reveal( namespace_id, reveal_addr, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount, privatekey, testset=self.testset )
def rpc_namespace_reveal_tx( self, namespace_id, reveal_addr, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount, privatekey ):
"""
Generate a signed transaction that will reveal and define the properties of a namespace.
Between the namespace definition and the "namespace begin" operation, only the
user who created the namespace can create names in it.
"""
return blockstack_namespace_reveal( namespace_id, reveal_addr, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount, privatekey, tx_only=True, testset=self.testset )
def rpc_namespace_ready( self, namespace_id, privatekey ):
"""
Declare that a namespace is open to accepting new names.
"""
return blockstack_namespace_ready( namespace_id, privatekey, testset=self.testset )
def rpc_namespace_ready_tx( self, namespace_id, privatekey ):
"""
Create a signed transaction that will declare that a namespace is open to accepting new names.
"""
return blockstack_namespace_ready( namespace_id, privatekey, tx_only=True, testset=self.testset )
def rpc_announce( self, message, privatekey ):
"""
announce a message to all blockstack nodes on the blockchain
@message is the message to send
@privatekey is the private key that will sign the announcement
Returns a JSON object with the transaction ID on success.
Returns a JSON object with 'error' on error.
"""
return blockstack_announce( str(message), str(privatekey), testset=self.testset )
def rpc_announce_tx( self, message, privatekey ):
"""
Generate a transaction that will make an announcement:
@message is the message text to send
@privatekey is the private key that signs the message
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_announce( str(message), str(privatekey), tx_only=True, testset=self.testset )
def rpc_announce_tx_subsidized( self, message, public_key, subsidy_key ):
"""
Generate a subsidizable transaction that will make an announcement
@message is hte message text to send
@privatekey is the private key that signs the message
Return a JSON object with the signed serialized transaction on success. It will not be broadcast.
Return a JSON object with 'error' on error.
"""
return blockstack_announce( str(message), None, user_public_key=str(user_public_key), subsidy_key=str(subsidy_key), tx_only=True, testset=self.testset )
def rpc_get_name_cost( self, name ):
"""
Return the cost of a given name, including fees
Return value is in satoshis
"""
# are we doing our initial indexing?
if len(name) > LENGTHS['blockchain_id_name']:
return {"error": "Name too long"}
ret = get_name_cost( name )
if ret is None:
if is_indexing():
return {"error": "Indexing blockchain"}
else:
return {"error": "Unknown/invalid namespace"}
return {"satoshis": int(math.ceil(ret))}
def rpc_get_namespace_cost( self, namespace_id ):
"""
Return the cost of a given namespace, including fees.
Return value is in satoshis
"""
if len(namespace_id) > LENGTHS['blockchain_id_namespace_id']:
return {"error": "Namespace ID too long"}
ret = price_namespace(namespace_id)
return {"satoshis": int(math.ceil(ret))}
def rpc_get_namespace_blockchain_record( self, namespace_id ):
"""
Return the readied namespace with the given namespace_id
"""
db = get_state_engine()
ns = db.get_namespace( namespace_id )
if ns is None:
if is_indexing():
return {"error": "Indexing blockchain"}
else:
return {"error": "No such ready namespace"}
else:
return ns
def rpc_get_all_names( self, offset, count ):
"""
Return all names
"""
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
db = get_state_engine()
return db.get_all_names( offset=offset, count=count )
def rpc_get_names_in_namespace( self, namespace_id, offset, count ):
"""
Return all names in a namespace
"""
# are we doing our initial indexing?
if is_indexing():
return {"error": "Indexing blockchain"}
db = get_state_engine()
return db.get_names_in_namespace( namespace_id, offset=offset, count=count )
def rpc_get_consensus_at( self, block_id ):
"""
Return the consensus hash at a block number
"""
db = get_state_engine()
return db.get_consensus_at( block_id )
def rpc_get_mutable_data( self, blockchain_id, data_name ):
"""
Get a mutable data record written by a given user.
"""
client = get_blockstack_client_session()
return client.get_mutable( str(blockchain_id), str(data_name) )
def rpc_get_immutable_data( self, blockchain_id, data_hash ):
"""
Get immutable data record written by a given user.
"""
client = get_blockstack_client_session()
return client.get_immutable( str(blockchain_id), str(data_hash) )
def rpc_get_block_from_consensus( self, consensus_hash ):
"""
Given the consensus hash, find the block number (or None)
"""
db = get_db_state()
return db.get_block_from_consensus( consensus_hash )
def rpc_get_zonefiles( self, zonefile_hashes ):
"""
Get a user's zonefile from the local cache,
or (on miss), from upstream storage.
Only return at most 100 zonefiles.
Return {'status': True, 'zonefiles': [zonefiles]} on success
Return {'error': ...} on error
"""
config = get_blockstack_opts()
if not config['serve_zonefiles']:
return {'error': 'No data'}
if len(zonefile_hashes) > 100:
return {'error': 'Too many requests'}
ret = {}
for zonefile_hash in zonefile_hashes:
if not is_current_zonefile_hash( zonefile_hash ):
continue
# check cache
cached_zonefile = get_cached_zonefile( zonefile_hash, zonefile_dir=config.get('zonefiles', None))
if cached_zonefile is not None:
ret[zonefile_hash] = cached_zonefile
continue
log.debug("Zonefile %s is not cached" % zonefile_hash)
try:
# check storage providers
zonefile = get_zonefile_from_storage( zonefile_hash )
except Exception, e:
log.exception(e)
continue
if zonefile is not None:
store_cached_zonefile( zonefile )
ret[zonefile_hash] = zonefile
return {'status': True, 'zonefiles': ret}
def rpc_put_zonefiles( self, zonefile_datas ):
"""
Replicate one or more zonefiles
Returns {'status': True, 'saved': [0|1]'} on success ('saved' is a vector of success/failure)
Returns {'error': ...} on error
Takes at most 10 zonefiles
"""
config = get_blockstack_opts()
if not config['serve_zonefiles']:
return {'error': 'No data'}
if len(zonefile_datas) > 100:
return {'error': 'Too many zonefiles'}
saved = []
for zonefile_data in zonefile_datas:
try:
zonefile_hash = blockstack_client.hash_zonefile( zonefile_data )
except:
log.debug("Invalid zonefile")
saved.append(0)
continue
if not is_current_zonefile_hash( zonefile_hash ):
log.debug("Unknown zonefile hash %s" % zonefile_hash)
saved.append(0)
continue
# it's a valid zonefile. cache and store it.
rc = store_cached_zonefile( zonefile_data )
if not rc:
log.debug("Failed to store zonefile %s" % zonefile_hash)
saved.append(0)
continue
rc = store_zonefile_to_storage( zonefile_data )
if not rc:
log.debug("Failed to replicate zonefile %s to external storage" % zonefile_hash)
saved.append(0)
continue
saved.append(1)
return {'status': True, 'saved': saved}
def rpc_get_inputs(self, blockchain_name, address):
"""
Proxy to UTXO provider to get an address's
unspent outputs.
"""
conf = get_blockstack_opts()
if not conf['blockchain_proxy']:
return {'error': 'No such method'}
inputs = get_tx_inputs( blockchain_name, None, address=address )
return inputs
def rpc_send_transaction(self, blockchain_name, txdata ):
"""
Proxy to UTXO provider to send a transaction
"""
conf = get_blockstack_opts()
if not conf['blockchain_proxy']:
return {'error': 'No such method'}
res = send_raw_transaction( blockchain_name, tx_data )
return res
class BlockstackdRPCServer( threading.Thread, object ):
"""
RPC server thread
"""
def __init__(self, port, testset=False):
super( BlockstackdRPCServer, self ).__init__()
self.testset = testset
self.rpc_server = None
self.port = port
def run(self):
"""
Serve until asked to stop
"""
self.rpc_server = BlockstackdRPC( port=self.port, testset=self.testset )
self.rpc_server.serve_forever()
def stop_server(self):
"""
Stop serving. Also stops the thread.
"""
self.rpc_server.shutdown()
def rpc_start( port, testset=False ):
"""
Start the global RPC server thread
"""
global rpc_server
rpc_server = BlockstackdRPCServer( port, testset=testset )
log.debug("Starting RPC")
rpc_server.start()
def rpc_stop():
"""
Stop the global RPC server thread
"""
global rpc_server
if rpc_server is not None:
log.debug("Shutting down RPC")
rpc_server.stop_server()
rpc_server.join()
log.debug("RPC joined")
def stop_server( clean=False, kill=False ):
"""
Stop the blockstackd server.
"""
# kill the main supervisor
pid_file = get_pidfile_path()
try:
fin = open(pid_file, "r")
except Exception, e:
pass
else:
pid_data = fin.read().strip()
fin.close()
pid = int(pid_data)
try:
os.kill(pid, signal.SIGTERM)
except OSError, oe:
if oe.errno == errno.ESRCH:
# already dead
log.info("Process %s is not running" % pid)
try:
os.unlink(pid_file)
except:
pass
return
except Exception, e:
log.exception(e)
sys.exit(1)
if kill:
clean = True
timeout = 5.0
log.info("Waiting %s seconds before sending SIGKILL to %s" % pid)
time.sleep(timeout)
try:
os.kill(pid, signal.SIGKILL)
except Exception, e:
pass
if clean:
# always blow away the pid file
try:
os.unlink(pid_file)
except:
pass
log.debug("Blockstack server stopped")
def get_indexing_lockfile():
"""
Return path to the indexing lockfile
"""
return os.path.join( virtualchain.get_working_dir(), "blockstack.indexing" )
def is_indexing():
"""
Is the blockstack daemon synchronizing with the blockchain?
"""
indexing_path = get_indexing_lockfile()
if os.path.exists( indexing_path ):
return True
else:
return False
def set_indexing( flag ):
"""
Set a flag in the filesystem as to whether or not we're indexing.
"""
indexing_path = get_indexing_lockfile()
if flag:
try:
fd = open( indexing_path, "w+" )
fd.close()
return True
except:
return False
else:
try:
os.unlink( indexing_path )
return True
except:
return False
def index_blockchain():
"""
Index the blockchain:
* find the range of blocks
* synchronize our state engine up to them
"""
bc_opts = get_blockchain_opts()
start_block, current_block = get_index_range()
if start_block is None and current_block is None:
log.error("Failed to find block range")
return
# bring us up to speed
log.debug("Begin indexing (up to %s)" % current_block)
set_indexing( True )
db = virtualchain_hooks.get_db_state()
virtualchain.sync_virtualchain( bc_opts, current_block, db )
set_indexing( False )
log.debug("End indexing (up to %s)" % current_block)
def blockstack_exit():
"""
Shut down the server on exit(3)
"""
stop_server(kill=True)
def blockstack_exit_handler( sig, frame ):
"""
Fatal signal handler
"""
sys.exit(0)
def run_server( testset=False, foreground=False, index=True ):
"""
Run the blockstackd RPC server, optionally in the foreground.
"""
blockstack_opts = get_blockstack_opts()
indexer_log_file = get_logfile_path() + ".indexer"
pid_file = get_pidfile_path()
working_dir = virtualchain.get_working_dir()
logfile = None
if not foreground:
try:
if os.path.exists( indexer_log_file ):
logfile = open( indexer_log_file, "a" )
else:
logfile = open( indexer_log_file, "a+" )
except OSError, oe:
log.error("Failed to open '%s': %s" % (indexer_log_file, oe.strerror))
sys.exit(1)
# become a daemon
child_pid = os.fork()
if child_pid == 0:
# child! detach, setsid, and make a new child to be adopted by init
sys.stdin.close()
os.dup2( logfile.fileno(), sys.stdout.fileno() )
os.dup2( logfile.fileno(), sys.stderr.fileno() )
os.setsid()
daemon_pid = os.fork()
if daemon_pid == 0:
# daemon!
os.chdir("/")
elif daemon_pid > 0:
# parent (intermediate child)
sys.exit(0)
else:
# error
sys.exit(1)
elif child_pid > 0:
# grand-parent
# wait for intermediate child
pid, status = os.waitpid( child_pid, 0 )
sys.exit(status)
# start API server
rpc_start(blockstack_opts['rpc_port'])
running = True
# put supervisor pid file
put_pidfile( pid_file, os.getpid() )
atexit.register( blockstack_exit )
if index:
# clear any stale indexing state
set_indexing( False )
log.debug("Begin Indexing")
while running:
try:
index_blockchain()
except Exception, e:
log.exception(e)
log.error("FATAL: caught exception while indexing")
sys.exit(1)
# wait for the next block
deadline = time.time() + REINDEX_FREQUENCY
while time.time() < deadline:
try:
time.sleep(1.0)
except:
# interrupt
running = False
break
else:
log.info("Not going to index, but will idle for testing")
while running:
try:
time.sleep(1.0)
except:
# interrupt
running = False
break
# stop API server
rpc_stop()
# close logfile
if logfile is not None:
logfile.flush()
logfile.close()
return 0
def setup( working_dir=None, testset=False, return_parser=False ):
"""
Do one-time initialization.
Call this to set up global state and set signal handlers.
If return_parser is True, return a partially-
setup argument parser to be populated with
subparsers (i.e. as part of main())
Otherwise return None.
"""
global blockstack_opts
global blockchain_client
global blockchain_broadcaster
global blockchain_opts
global service_opts
# set up our implementation
if working_dir is not None:
if not os.path.exists( working_dir ):
os.makedirs( working_dir, 0700 )
blockstack_state_engine.working_dir = working_dir
virtualchain.setup_virtualchain( blockstack_state_engine, testset=testset )
testset_path = get_testset_filename( working_dir )
if testset:
# flag testset so our subprocesses see it
if not os.path.exists( testset_path ):
with open( testset_path, "w+" ) as f:
pass
else:
# flag not set
if os.path.exists( testset_path ):
os.unlink( testset_path )
# acquire configuration, and store it globally
blockstack_opts, bitcoin_opts, utxo_opts, other_blockchain_opts, other_service_opts = configure( interactive=True, testset=testset )
# do we need to enable testset?
if bitcoin_opts['testset']:
virtualchain.setup_virtualchain( blockstack_state_engine, testset=True )
testset = True
# if we're using the mock UTXO provider, then switch to the mock bitcoind node as well
if utxo_opts['service_provider'] == 'mock_utxo':
import blockstack_integration_tests.mock_bitcoind
virtualchain.setup_virtualchain( blockstack_state_engine, testset=testset, blockchain_connection_factory=blockstack_integration_tests.mock_bitcoind.connect_mock_bitcoind )
# merge in command-line blockchain options
config_file = virtualchain.get_config_filename()
arg_bitcoin_opts = None
argparser = None
if return_parser:
arg_bitcoin_opts, argparser = virtualchain.parse_blockchain_args( "bitcoin", return_parser=return_parser )
else:
arg_bitcoin_opts = virtualchain.parse_blockchain_args( "bitcoin", return_parser=return_parser )
# command-line overrides config file
for (k, v) in arg_bitcoin_opts.items():
bitcoin_opts[k] = v
# store options
set_blockstack_opts( blockstack_opts )
set_blockchain_opts( "bitcoin", bitcoin_opts )
set_service_opts( "bitcoin", utxo_opts )
for blockchain_name in other_blockchain_opts:
set_blockchain_opts( blockchain_name, other_blockchain_opts[blockchain_name] )
for blockchain_name in other_service_opts:
set_service_opts( blockchain_name, other_service_opts[blockchain_name] )
if return_parser:
return argparser
else:
return None
def reconfigure( testset=False ):
"""
Reconfigure blockstackd.
"""
configure( force=True, testset=testset )
print "Blockstack successfully reconfigured."
sys.exit(0)
def clean( testset=False, confirm=True ):
"""
Remove blockstack's db, lastblock, and snapshot files.
Prompt for confirmation
"""
delete = False
exit_status = 0
if confirm:
warning = "WARNING: THIS WILL DELETE YOUR BLOCKSTACK DATABASE!\n"
warning+= "Database: '%s'\n" % blockstack_state_engine.working_dir
warning+= "Are you sure you want to proceed?\n"
warning+= "Type 'YES' if so: "
value = raw_input( warning )
if value != "YES":
sys.exit(exit_status)
else:
delete = True
else:
delete = True
if delete:
print "Deleting..."
db_filename = virtualchain.get_db_filename()
lastblock_filename = virtualchain.get_lastblock_filename()
snapshots_filename = virtualchain.get_snapshots_filename()
for path in [db_filename, lastblock_filename, snapshots_filename]:
try:
os.unlink( path )
except:
log.warning("Unable to delete '%s'" % path)
exit_status = 1
sys.exit(exit_status)
def rec_to_virtualchain_op( name_rec, block_number, history_index, untrusted_db, testset=False ):
"""
Given a record from the blockstack database,
convert it into a virtualchain operation to
process.
"""
# apply opcodes so we can consume them with virtualchain
opcode_name = str(name_rec['opcode'])
ret_op = {}
if name_rec.has_key('expired') and name_rec['expired']:
# don't care
return None
if opcode_name == "NAME_PREORDER":
name_rec_script = build_preorder( None, None, None, str(name_rec['consensus_hash']), name_hash=str(name_rec['preorder_name_hash']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_preorder( name_rec_payload )
elif opcode_name == "NAME_REGISTRATION":
name_rec_script = build_registration( str(name_rec['name']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_registration( name_rec_payload )
# reconstruct the registration op...
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
# restore history to find prevoius sender and address
untrusted_name_rec = untrusted_db.get_name( str(name_rec['name']) )
name_rec['history'] = untrusted_name_rec['history']
if history_index > 0:
print "restore from %s" % block_number
name_rec_prev = BlockstackDB.restore_from_history( name_rec, block_number )[ history_index - 1 ]
else:
print "restore from %s" % (block_number - 1)
name_rec_prev = BlockstackDB.restore_from_history( name_rec, block_number - 1 )[ history_index - 1 ]
sender = name_rec_prev['sender']
address = name_rec_prev['address']
ret_op['sender'] = sender
ret_op['address'] = address
del name_rec['history']
elif opcode_name == "NAME_UPDATE":
data_hash = None
if name_rec['value_hash'] is not None:
data_hash = str(name_rec['value_hash'])
name_rec_script = build_update( str(name_rec['name']), str(name_rec['consensus_hash']), data_hash=data_hash, testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_update(name_rec_payload)
elif opcode_name == "NAME_TRANSFER":
# reconstruct the transfer op...
KEEPDATA_OP = "%s%s" % (NAME_TRANSFER, TRANSFER_KEEP_DATA)
if name_rec['op'] == KEEPDATA_OP:
name_rec['keep_data'] = True
else:
name_rec['keep_data'] = False
# what was the previous owner?
recipient = str(name_rec['sender'])
recipient_address = str(name_rec['address'])
# restore history
untrusted_name_rec = untrusted_db.get_name( str(name_rec['name']) )
name_rec['history'] = untrusted_name_rec['history']
prev_block_number = None
prev_history_index = None
# get previous owner
if history_index > 0:
name_rec_prev = BlockstackDB.restore_from_history( name_rec, block_number )[history_index - 1]
prev_block_number = block_number
prev_history_index = history_index-1
else:
name_rec_prev = BlockstackDB.restore_from_history( name_rec, block_number - 1 )[history_index - 1]
prev_block_number = block_number-1
prev_history_index = history_index-1
if 'transfer_send_block_id' not in name_rec:
log.error("FATAL: Obsolete or invalid database. Missing 'transfer_send_block_id' field for NAME_TRANSFER at (%s, %s)" % (block_number, history_index))
sys.exit(1)
sender = name_rec_prev['sender']
address = name_rec_prev['address']
send_block_id = name_rec['transfer_send_block_id']
# reconstruct recipient and sender
name_rec['recipient'] = recipient
name_rec['recipient_address'] = recipient_address
name_rec['sender'] = sender
name_rec['address'] = address
name_rec['consensus_hash'] = untrusted_db.get_consensus_at( send_block_id )
name_rec_script = build_transfer( str(name_rec['name']), name_rec['keep_data'], str(name_rec['consensus_hash']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_transfer(name_rec_payload, name_rec['recipient'] )
del name_rec['history']
elif opcode_name == "NAME_REVOKE":
name_rec_script = build_revoke( str(name_rec['name']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_revoke( name_rec_payload )
elif opcode_name == "NAME_IMPORT":
name_rec_script = build_name_import( str(name_rec['name']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
# reconstruct recipient and importer
name_rec['recipient'] = str(name_rec['sender'])
name_rec['recipient_address'] = str(name_rec['address'])
name_rec['sender'] = str(name_rec['importer'])
name_rec['address'] = str(name_rec['importer_address'])
ret_op = parse_name_import( name_rec_payload, str(name_rec['recipient']), str(name_rec['value_hash']) )
elif opcode_name == "NAMESPACE_PREORDER":
name_rec_script = build_namespace_preorder( None, None, None, str(name_rec['consensus_hash']), namespace_id_hash=str(name_rec['namespace_id_hash']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_namespace_preorder(name_rec_payload)
elif opcode_name == "NAMESPACE_REVEAL":
name_rec_script = build_namespace_reveal( str(name_rec['namespace_id']), name_rec['version'], str(name_rec['recipient_address']), \
name_rec['lifetime'], name_rec['coeff'], name_rec['base'], name_rec['buckets'],
name_rec['nonalpha_discount'], name_rec['no_vowel_discount'], testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_namespace_reveal( name_rec_payload, str(name_rec['sender']), str(name_rec['recipient_address']) )
elif opcode_name == "NAMESPACE_READY":
name_rec_script = build_namespace_ready( str(name_rec['namespace_id']), testset=testset )
name_rec_payload = binascii.unhexlify( name_rec_script )[3:]
ret_op = parse_namespace_ready( name_rec_payload )
ret_op = virtualchain.virtualchain_set_opfields( ret_op, virtualchain_opcode=getattr( config, opcode_name ), virtualchain_txid=str(name_rec['txid']), virtualchain_txindex=int(name_rec['vtxindex']) )
ret_op['opcode'] = opcode_name
merged_ret_op = copy.deepcopy( name_rec )
merged_ret_op.update( ret_op )
return merged_ret_op
def find_last_transfer_consensus_hash( name_rec, block_id, vtxindex ):
"""
Given a name record, find the last non-NAME_TRANSFER consensus hash.
Return None if not found.
"""
history_keys = name_rec['history'].keys()
history_keys.sort()
history_keys.reverse()
for hk in history_keys:
if hk > block_id:
continue
history_states = BlockstackDB.restore_from_history( name_rec, hk )
for history_state in reversed(history_states):
if hk == block_id and history_state['vtxindex'] > vtxindex:
# from the future
continue
if history_state['op'][0] == NAME_TRANSFER:
# skip NAME_TRANSFERS
continue
if history_state['op'][0] in [NAME_IMPORT, NAME_REGISTRATION]:
# out of history
return None
if history_state.has_key('consensus_hash') and history_state['consensus_hash'] is not None:
return history_state['consensus_hash']
return None
def nameop_restore_consensus_fields( name_rec, block_id ):
"""
Given a nameop at a point in time, ensure
that all of its consensus fields are present.
Because they can be reconstructed directly from the nameop,
but they are not always stored in the db.
"""
opcode_name = str(name_rec['opcode'])
ret_op = {}
if opcode_name == "NAME_REGISTRATION":
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
elif opcode_name == "NAME_IMPORT":
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
elif opcode_name == "NAME_TRANSFER":
db = get_state_engine()
if 'transfer_send_block_id' not in name_rec:
log.error("FATAL: Obsolete or invalid database. Missing 'transfer_send_block_id' field for NAME_TRANSFER at (%s, %s)" % (prev_block_number, prev_history_index))
sys.exit(1)
full_rec = db.get_name( name_rec['name'], include_expired=True )
full_history = full_rec['history']
# reconstruct the recipient information
ret_op['recipient'] = str(name_rec['sender'])
ret_op['recipient_address'] = str(name_rec['address'])
# reconstruct name_hash, consensus_hash, keep_data
keep_data = None
if name_rec['op'][-1] == TRANSFER_KEEP_DATA:
keep_data = True
else:
keep_data = False
old_history = name_rec.get('history', None)
name_rec['history'] = full_history
consensus_hash = find_last_transfer_consensus_hash( name_rec, block_id, name_rec['vtxindex'] )
name_rec['history'] = old_history
ret_op['keep_data'] = keep_data
if consensus_hash is not None:
print "restore consensus hash (%s,%s): %s" % (block_id, name_rec['vtxindex'], consensus_hash)
ret_op['consensus_hash'] = consensus_hash
else:
ret_op['consensus_hash'] = db.get_consensus_at( name_rec['transfer_send_block_id'] )
print "Use consensus hash from %s: %s" % (name_rec['transfer_send_block_id'], ret_op['consensus_hash'])
ret_op['name_hash'] = hash256_trunc128( str(name_rec['name']) )
elif opcode_name == "NAME_UPDATE":
# reconstruct name_hash
ret_op['name_hash'] = hash256_trunc128( str(name_rec['name']) + str(name_rec['consensus_hash']) )
elif opcode_name == "NAME_REVOKE":
ret_op['revoked'] = True
ret_op = virtualchain.virtualchain_set_opfields( ret_op, virtualchain_opcode=getattr( config, opcode_name ), virtualchain_txid=str(name_rec['txid']), virtualchain_txindex=int(name_rec['vtxindex']) )
ret_op['opcode'] = opcode_name
merged_op = copy.deepcopy( name_rec )
merged_op.update( ret_op )
if 'name_hash' in merged_op.keys():
nh = merged_op['name_hash']
merged_op['name_hash128'] = nh
return merged_op
def block_to_virtualchain_ops( block_id, db ):
"""
convert a block's name ops to virtualchain ops.
This is needed in order to recreate the virtualchain
transactions that generated the block's name operations,
such as for re-building the db or serving SNV clients.
Returns the list of virtualchain ops.
"""
# all sequences of operations at this block, in tx order
nameops = db.get_all_nameops_at( block_id )
virtualchain_ops = []
# process nameops in order by vtxindex
nameops = sorted( nameops, key=lambda op: op['vtxindex'] )
# each name record has its own history, and their interleaving in tx order
# is what makes up nameops. However, when restoring a name record to
# a previous state, we need to know the *relative* order of operations
# that changed it during this block. This is called the history index,
# and it maps names to a dict, which maps the the virtual tx index (vtxindex)
# to integer h such that nameops[name][vtxindex] is the hth update to the name
# record.
history_index = {}
for i in xrange(0, len(nameops)):
nameop = nameops[i]
if 'name' not in nameop.keys():
continue
name = str(nameop['name'])
if name not in history_index.keys():
history_index[name] = { i: 0 }
else:
history_index[name][i] = max( history_index[name].values() ) + 1
for i in xrange(0, len(nameops)):
# only trusted fields
opcode_name = nameops[i]['opcode']
consensus_fields = SERIALIZE_FIELDS.get( opcode_name, None )
if consensus_fields is None:
raise Exception("BUG: no consensus fields defined for '%s'" % opcode_name )
# coerce string, not unicode
for k in nameops[i].keys():
if type(nameops[i][k]) == unicode:
nameops[i][k] = str(nameops[i][k])
# remove virtualchain-specific fields--they won't be trusted
nameops[i] = db.sanitize_op( nameops[i] )
for field in nameops[i].keys():
# remove untrusted fields, except for:
# * 'opcode' (which will be fed into the consensus hash
# indirectly, once the fields are successfully processed and thus proven consistent with
# the fields),
# * 'transfer_send_block_id' (which will be used to find the NAME_TRANSFER consensus hash,
# thus indirectly feeding this information into the consensus hash as well).
if field not in consensus_fields and field not in ['opcode', 'transfer_send_block_id']:
log.warning("OP '%s': Removing untrusted field '%s'" % (opcode_name, field))
del nameops[i][field]
try:
# recover virtualchain op from name record
h = 0
if 'name' in nameops[i]:
if nameops[i]['name'] in history_index:
h = history_index[ nameops[i]['name'] ][i]
virtualchain_op = rec_to_virtualchain_op( nameops[i], block_id, h, db )
except:
print json.dumps( nameops[i], indent=4 )
raise
if virtualchain_op is not None:
virtualchain_ops.append( virtualchain_op )
return virtualchain_ops
def rebuild_database( target_block_id, untrusted_db_path, working_db_path=None, resume_dir=None, start_block=None, testset=False ):
"""
Given a target block ID and a path to an (untrusted) db, reconstruct it in a temporary directory by
replaying all the nameops it contains.
Return the consensus hash calculated at the target block.
"""
# reconfigure the virtualchain to use a temporary directory,
# so we don't interfere with this instance's primary database
working_dir = None
if resume_dir is None:
working_dir = tempfile.mkdtemp( prefix='blockstack-verify-database-' )
else:
working_dir = resume_dir
blockstack_state_engine.working_dir = working_dir
virtualchain.setup_virtualchain( blockstack_state_engine, testset=testset )
if resume_dir is None:
# not resuming
start_block = virtualchain.get_first_block_id()
else:
# resuming
old_start_block = start_block
start_block = get_lastblock()
if start_block is None:
start_block = old_start_block
log.debug( "Rebuilding database from %s to %s" % (start_block, target_block_id) )
# feed in operations, block by block, from the untrusted database
untrusted_db = BlockstackDB( untrusted_db_path )
# working db, to build up the operations in the untrusted db block-by-block
working_db = None
if working_db_path is None:
working_db_path = virtualchain.get_db_filename()
working_db = BlockstackDB( working_db_path )
log.debug( "Working DB: %s" % working_db_path )
log.debug( "Untrusted DB: %s" % untrusted_db_path )
# map block ID to consensus hashes
consensus_hashes = {}
for block_id in xrange( start_block, target_block_id+1 ):
virtualchain_ops = block_to_virtualchain_ops( block_id, untrusted_db )
# feed ops to virtualchain to reconstruct the db at this block
consensus_hash = working_db.process_block( block_id, virtualchain_ops )
log.debug("VERIFY CONSENSUS(%s): %s" % (block_id, consensus_hash))
consensus_hashes[block_id] = consensus_hash
# final consensus hash
return consensus_hashes[ target_block_id ]
def verify_database( trusted_consensus_hash, consensus_block_id, untrusted_db_path, working_db_path=None, start_block=None, testset=False ):
"""
Verify that a database is consistent with a
known-good consensus hash.
This algorithm works by creating a new database,
parsing the untrusted database, and feeding the untrusted
operations into the new database block-by-block. If we
derive the same consensus hash, then we can trust the
database.
"""
final_consensus_hash = rebuild_database( consensus_block_id, untrusted_db_path, working_db_path=working_db_path, start_block=start_block, testset=testset )
# did we reach the consensus hash we expected?
if final_consensus_hash == trusted_consensus_hash:
return True
else:
log.error("Unverifiable database state stored in '%s'" % blockstack_state_engine.working_dir )
return False
def restore( working_dir, block_number ):
"""
Restore the database from a backup in the backups/ directory.
If block_number is None, then use the latest backup.
Raise an exception if no such backup exists
"""
if block_number is None:
all_blocks = BlockstackDB.get_backup_blocks( virtualchain_hooks )
if len(all_blocks) == 0:
log.error("No backups available")
return False
block_number = max(all_blocks)
found = True
backup_paths = BlockstackDB.get_backup_paths( block_number, virtualchain_hooks )
for p in backup_paths:
if not os.path.exists(p):
log.error("Missing backup file: '%s'" % p)
found = False
if not found:
return False
rc = BlockstackDB.backup_restore( block_number, virtualchain_hooks )
if not rc:
log.error("Failed to restore backup")
return rc
def check_testset_enabled():
"""
Check sys.argv to see if testset is enabled.
Must be done before we initialize the virtual chain.
"""
for arg in sys.argv:
if arg == "--testset":
return True
return False
def check_alternate_working_dir():
"""
Check sys.argv to see if there is an alternative
working directory selected. We need to know this
before setting up the virtual chain.
"""
path = None
for i in xrange(0, len(sys.argv)):
arg = sys.argv[i]
if arg.startswith('--working-dir'):
if '=' in arg:
argparts = arg.split("=")
arg = argparts[0]
parts = argparts[1:]
path = "=".join(parts)
elif i + 1 < len(sys.argv):
path = sys.argv[i+1]
else:
print >> sys.stderr, "--working-dir requires an argument"
return None
return path
def run_blockstackd():
"""
run blockstackd
"""
testset = check_testset_enabled()
working_dir = check_alternate_working_dir()
argparser = setup( testset=testset, working_dir=working_dir, return_parser=True )
# get RPC server options
subparsers = argparser.add_subparsers(
dest='action', help='the action to be taken')
parser = subparsers.add_parser(
'start',
help='start the blockstackd server')
parser.add_argument(
'--foreground', action='store_true',
help='start the blockstackd server in foreground')
parser.add_argument(
'--testset', action='store_true',
help='run with the set of name operations used for testing, instead of the main set')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser.add_argument(
'--no-index', action='store_true',
help='do not index the blockchain, but only run an RPC endpoint')
parser = subparsers.add_parser(
'stop',
help='stop the blockstackd server')
parser.add_argument(
'--testset', action='store_true',
help='required if the daemon is using the testing set of name operations')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'reconfigure',
help='reconfigure the blockstackd server')
parser.add_argument(
'--testset', action='store_true',
help='required if the daemon is using the testing set of name operations')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'clean',
help='remove all blockstack database information')
parser.add_argument(
'--force', action='store_true',
help='Do not confirm the request to delete.')
parser.add_argument(
'--testset', action='store_true',
help='required if the daemon is using the testing set of name operations')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'restore',
help="Restore the database from a backup")
parser.add_argument(
'block_number', nargs='?',
help="The block number to restore from (if not given, the last backup will be used)")
parser = subparsers.add_parser(
'rebuilddb',
help='Reconstruct the current database from particular block number by replaying all prior name operations')
parser.add_argument(
'db_path',
help='the path to the database')
parser.add_argument(
'start_block_id',
help='the block ID from which to start rebuilding')
parser.add_argument(
'end_block_id',
help='the block ID at which to stop rebuilding')
parser.add_argument(
'--resume-dir', nargs='?',
help='the temporary directory to store the database state as it is being rebuilt. Blockstackd will resume working from this directory if it is interrupted.')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'verifydb',
help='verify an untrusted database against a known-good consensus hash')
parser.add_argument(
'block_id',
help='the block ID of the known-good consensus hash')
parser.add_argument(
'consensus_hash',
help='the known-good consensus hash')
parser.add_argument(
'db_path',
help='the path to the database')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'importdb',
help='import an existing trusted database')
parser.add_argument(
'db_path',
help='the path to the database')
parser.add_argument(
'--working-dir', action='store',
help='use an alternative working directory')
parser = subparsers.add_parser(
'version',
help='Print version and exit')
args, _ = argparser.parse_known_args()
if args.action == 'version':
print "Blockstack version: %s.%s" % (VERSION, BLOCKSTACK_VERSION)
print "Testset: %s" % testset
sys.exit(0)
if args.action == 'start':
if os.path.exists( get_pidfile_path() ):
log.error("Blockstackd appears to be running already. If not, please run '%s stop'" % (sys.argv[0]))
sys.exit(1)
if args.foreground:
log.info('Initializing blockstackd server in foreground (testset = %s, working dir = \'%s\')...' % (testset, working_dir))
else:
log.info('Starting blockstackd server (testset = %s, working_dir = \'%s\') ...' % (testset, working_dir))
if args.no_index:
log.info("Not indexing the blockchain; only running an RPC endpoint")
exit_status = run_server( foreground=args.foreground, testset=testset, index=(not args.no_index) )
if args.foreground:
log.info("Service endpoint exited with status code %s" % exit_status )
elif args.action == 'stop':
stop_server(kill=True)
elif args.action == 'reconfigure':
reconfigure( testset=testset )
elif args.action == 'restore':
restore( working_dir, args.block_number )
elif args.action == 'clean':
clean( confirm=(not args.force), testset=args.testset )
elif args.action == 'rebuilddb':
resume_dir = None
if hasattr(args, 'resume_dir') and args.resume_dir is not None:
resume_dir = args.resume_dir
final_consensus_hash = rebuild_database( int(args.end_block_id), args.db_path, start_block=int(args.start_block_id), resume_dir=resume_dir )
print "Rebuilt database in '%s'" % blockstack_state_engine.working_dir
print "The final consensus hash is '%s'" % final_consensus_hash
elif args.action == 'verifydb':
rc = verify_database( args.consensus_hash, int(args.block_id), args.db_path )
if rc:
# success!
print "Database is consistent with %s" % args.consensus_hash
print "Verified files are in '%s'" % blockstack_state_engine.working_dir
else:
# failure!
print "Database is NOT CONSISTENT"
elif args.action == 'importdb':
old_working_dir = blockstack_state_engine.working_dir
blockstack_state_engine.working_dir = None
virtualchain.setup_virtualchain( blockstack_state_engine, testset=testset )
db_path = virtualchain.get_db_filename()
old_snapshots_path = os.path.join( old_working_dir, os.path.basename( virtualchain.get_snapshots_filename() ) )
old_lastblock_path = os.path.join( old_working_dir, os.path.basename( virtualchain.get_lastblock_filename() ) )
if os.path.exists( db_path ):
print "Backing up existing database to %s.bak" % db_path
shutil.move( db_path, db_path + ".bak" )
print "Importing database from %s to %s" % (args.db_path, db_path)
shutil.copy( args.db_path, db_path )
print "Importing snapshots from %s to %s" % (old_snapshots_path, virtualchain.get_snapshots_filename() )
shutil.copy( old_snapshots_path, virtualchain.get_snapshots_filename() )
print "Importing lastblock from %s to %s" % (old_lastblock_path, virtualchain.get_lastblock_filename() )
shutil.copy( old_lastblock_path, virtualchain.get_lastblock_filename() )
# clean up
shutil.rmtree( old_working_dir )
if os.path.exists( old_working_dir ):
os.rmdir( old_working_dir )
if __name__ == '__main__':
run_blockstackd()