mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-01-13 08:40:45 +08:00
2874 lines
99 KiB
Python
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()
|