mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-06-15 17:17:45 +08:00
1124 lines
32 KiB
Python
1124 lines
32 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstore
|
|
~~~~~
|
|
copyright: (c) 2014 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2015 by Blockstack.org
|
|
|
|
This file is part of Blockstore
|
|
|
|
Blockstore is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Blockstore is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with Blockstore. If not, see <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
|
|
|
|
from ConfigParser import SafeConfigParser
|
|
|
|
import pybitcoin
|
|
from txjsonrpc.netstring import jsonrpc
|
|
|
|
from lib import nameset as blockstore_state_engine
|
|
from lib import get_db_state
|
|
from lib.config import REINDEX_FREQUENCY, TESTSET, DEFAULT_DUST_FEE
|
|
from lib import *
|
|
|
|
import virtualchain
|
|
log = virtualchain.session.log
|
|
|
|
# global variables, for use with the RPC server and the twisted callback
|
|
blockstore_opts = None
|
|
bitcoind = None
|
|
bitcoin_opts = None
|
|
utxo_opts = None
|
|
blockchain_client = None
|
|
blockchain_broadcaster = None
|
|
indexer_pid = None
|
|
|
|
def get_bitcoind( new_bitcoind_opts=None, reset=False, new=False ):
|
|
"""
|
|
Get or instantiate our bitcoind client.
|
|
Optionally re-set the bitcoind options.
|
|
"""
|
|
global bitcoind
|
|
global bitcoin_opts
|
|
|
|
if reset:
|
|
bitcoind = None
|
|
|
|
elif not new and bitcoind is not None:
|
|
return bitcoind
|
|
|
|
if new or bitcoind is None:
|
|
if new_bitcoind_opts is not None:
|
|
bitcoin_opts = new_bitcoind_opts
|
|
|
|
new_bitcoind = None
|
|
try:
|
|
new_bitcoind = virtualchain.connect_bitcoind( bitcoin_opts )
|
|
|
|
if new:
|
|
return new_bitcoind
|
|
|
|
else:
|
|
# save for subsequent reuse
|
|
bitcoind = new_bitcoind
|
|
return bitcoind
|
|
|
|
except Exception, e:
|
|
log.exception( e )
|
|
return None
|
|
|
|
|
|
def get_bitcoin_opts():
|
|
"""
|
|
Get the bitcoind connection arguments.
|
|
"""
|
|
|
|
global bitcoin_opts
|
|
return bitcoin_opts
|
|
|
|
|
|
def get_utxo_opts():
|
|
"""
|
|
Get UTXO provider options.
|
|
"""
|
|
global utxo_opts
|
|
return utxo_opts
|
|
|
|
|
|
def get_blockstore_opts():
|
|
"""
|
|
Get blockstore configuration options.
|
|
"""
|
|
global blockstore_opts
|
|
return blockstore_opts
|
|
|
|
|
|
def set_bitcoin_opts( new_bitcoin_opts ):
|
|
"""
|
|
Set new global bitcoind operations
|
|
"""
|
|
global bitcoin_opts
|
|
bitcoin_opts = new_bitcoin_opts
|
|
|
|
|
|
def set_utxo_opts( new_utxo_opts ):
|
|
"""
|
|
Set new global chian.com options
|
|
"""
|
|
global utxo_opts
|
|
utxo_opts = new_utxo_opts
|
|
|
|
|
|
def get_pidfile_path():
|
|
"""
|
|
Get the PID file path.
|
|
"""
|
|
working_dir = virtualchain.get_working_dir()
|
|
pid_filename = blockstore_state_engine.get_virtual_chain_name() + ".pid"
|
|
return os.path.join( working_dir, pid_filename )
|
|
|
|
|
|
def get_tacfile_path():
|
|
"""
|
|
Get the TAC file path for our service endpoint.
|
|
Should be in the same directory as this module.
|
|
"""
|
|
working_dir = os.path.abspath(os.path.dirname(__file__))
|
|
tac_filename = blockstore_state_engine.get_virtual_chain_name() + ".tac"
|
|
return os.path.join( working_dir, tac_filename )
|
|
|
|
|
|
def get_logfile_path():
|
|
"""
|
|
Get the logfile path for our service endpoint.
|
|
"""
|
|
working_dir = virtualchain.get_working_dir()
|
|
logfile_filename = blockstore_state_engine.get_virtual_chain_name() + ".log"
|
|
return os.path.join( working_dir, logfile_filename )
|
|
|
|
|
|
def get_state_engine():
|
|
"""
|
|
Get a handle to the blockstore virtual chain state engine.
|
|
"""
|
|
return get_db_state()
|
|
|
|
|
|
def get_index_range():
|
|
"""
|
|
Get the bitcoin block index range.
|
|
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.
|
|
"""
|
|
|
|
bitcoind_session = get_bitcoind( new=True )
|
|
|
|
first_block = None
|
|
last_block = None
|
|
while last_block is None:
|
|
|
|
first_block, last_block = virtualchain.get_index_range( bitcoind_session )
|
|
|
|
if last_block is None:
|
|
|
|
# try to reconnnect
|
|
time.sleep(1)
|
|
log.error("Reconnect to bitcoind")
|
|
bitcoind_session = get_bitcoind( new=True )
|
|
continue
|
|
|
|
else:
|
|
return first_block, last_block - NUM_CONFIRMATIONS
|
|
|
|
|
|
def die_handler_server(signal, frame):
|
|
"""
|
|
Handle Ctrl+C for server subprocess
|
|
"""
|
|
|
|
log.info('Exiting blockstored server')
|
|
stop_server()
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
def die_handler_indexer(signal, frame):
|
|
"""
|
|
Handle Ctrl+C for indexer processe
|
|
"""
|
|
|
|
db = get_state_engine()
|
|
virtualchain.stop_sync_virtualchain( db )
|
|
sys.exit(0)
|
|
|
|
|
|
def json_traceback():
|
|
exception_data = traceback.format_exc().splitlines()
|
|
return {
|
|
"error": exception_data[-1],
|
|
"traceback": exception_data
|
|
}
|
|
|
|
|
|
def get_utxo_provider_client():
|
|
"""
|
|
Get or instantiate our blockchain UTXO provider's client.
|
|
Return None if we were unable to connect
|
|
"""
|
|
|
|
# acquire configuration (which we should already have)
|
|
blockstore_opts, blockchain_opts, utxo_opts, dht_opts = configure( interactive=False )
|
|
|
|
try:
|
|
blockchain_client = connect_utxo_provider( utxo_opts )
|
|
return blockchain_client
|
|
except:
|
|
log.exception(e)
|
|
return None
|
|
|
|
|
|
def get_tx_broadcaster():
|
|
"""
|
|
Get or instantiate our blockchain UTXO provider's transaction broadcaster.
|
|
fall back to the utxo provider client, if one is not designated
|
|
"""
|
|
|
|
# acquire configuration (which we should already have)
|
|
blockstore_opts, blockchain_opts, utxo_opts, dht_opts = configure( interactive=False )
|
|
|
|
# is there a particular blockchain client we want for importing?
|
|
if 'tx_broadcaster' not in blockstore_opts:
|
|
return get_utxo_provider_client()
|
|
|
|
broadcaster_opts = default_utxo_provider_opts( blockstore_opts['tx_broadcaster'] )
|
|
|
|
try:
|
|
blockchain_broadcaster = connect_utxo_provider( broadcaster_opts )
|
|
return blockchain_broadcaster
|
|
except:
|
|
log.exception(e)
|
|
return None
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
class BlockstoredRPC(jsonrpc.JSONRPC):
|
|
"""
|
|
Blockstored not-quote-JSON-RPC server.
|
|
|
|
We say "not quite" because the implementation serves data
|
|
via Netstrings, not HTTP, and does not pay attention to
|
|
the 'id' or 'version' fields in the JSONRPC spec.
|
|
|
|
This endpoint does *not* talk to a storage provider, but only
|
|
serves back information from the blockstore virtual chain.
|
|
|
|
The client is responsible for resolving this information
|
|
to data, via an ancillary storage provider.
|
|
"""
|
|
|
|
def jsonrpc_ping(self):
|
|
reply = {}
|
|
reply['status'] = "alive"
|
|
return reply
|
|
|
|
def jsonrpc_lookup(self, name):
|
|
"""
|
|
Lookup the profile for a name.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockstore_state_engine = get_state_engine()
|
|
name_record = blockstore_state_engine.get_name( name )
|
|
|
|
if name is None:
|
|
return {"error": "Not found."}
|
|
|
|
else:
|
|
return name_record
|
|
|
|
|
|
def jsonrpc_getinfo(self):
|
|
"""
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
bitcoind = get_bitcoind()
|
|
info = bitcoind.getinfo()
|
|
reply = {}
|
|
reply['blocks'] = info['blocks']
|
|
|
|
db = get_state_engine()
|
|
reply['consensus'] = db.get_current_consensus()
|
|
return reply
|
|
|
|
|
|
def jsonrpc_preorder(self, name, register_addr, privatekey):
|
|
""" Preorder a name
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
db = get_state_engine()
|
|
consensus_hash = db.get_current_consensus()
|
|
|
|
if not consensus_hash:
|
|
# 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"}
|
|
|
|
namespace_id = get_namespace_from_name( name )
|
|
|
|
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))
|
|
|
|
try:
|
|
resp = preorder_name(str(name), str(register_addr), str(consensus_hash), str(privatekey), blockchain_client_inst, name_fee, testset=TESTSET)
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug('preorder <%s, %s>' % (name, privatekey))
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_register(self, name, register_addr, privatekey, renewal_fee=None):
|
|
""" Register a name
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
log.info("name: %s" % name)
|
|
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"}
|
|
|
|
try:
|
|
resp = register_name(str(name), str(register_addr), str(privatekey), blockchain_client_inst, renewal_fee=renewal_fee, testset=TESTSET)
|
|
except:
|
|
return json_traceback()
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_update(self, name, data_hash, privatekey):
|
|
"""
|
|
Update a name with new data.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
log.debug('update <%s, %s, %s>' % (name, data_hash, privatekey))
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
db = get_state_engine()
|
|
|
|
consensus_hash = db.get_current_consensus()
|
|
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
try:
|
|
resp = update_name(str(name), str(data_hash), str(consensus_hash), str(privatekey), blockchain_client_inst, testset=TESTSET)
|
|
except:
|
|
return json_traceback()
|
|
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_transfer(self, name, address, keep_data, privatekey):
|
|
""" Transfer a name
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
db = get_state_engine()
|
|
|
|
consensus_hash = db.get_current_consensus()
|
|
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
try:
|
|
resp = transfer_name(str(name), str(address), bool(keep_data), str(consensus_hash), str(privatekey), blockchain_client_inst, testset=TESTSET)
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug('transfer <%s, %s, %s>' % (name, address, privatekey))
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_renew(self, name, privatekey):
|
|
""" Renew a name
|
|
"""
|
|
|
|
# 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
|
|
register_addr = name_rec['address']
|
|
renewal_fee = get_name_cost( name )
|
|
|
|
return self.jsonrpc_register( name, register_addr, privatekey, renewal_fee=renewal_fee )
|
|
|
|
|
|
def jsonrpc_revoke( self, name, privatekey ):
|
|
""" Revoke a name and all of its data.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
try:
|
|
resp = revoke_name(str(name), str(privatekey), blockchain_client_inst, testset=TESTSET)
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug("revoke <%s>" % name )
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_name_import( self, name, recipient_address, update_hash, privatekey ):
|
|
"""
|
|
Import a name into a namespace.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
broadcaster_client_inst = get_tx_broadcaster()
|
|
if broadcaster_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain transaction broadcaster"}
|
|
|
|
db = get_state_engine()
|
|
|
|
try:
|
|
resp = name_import( str(name), str(recipient_address), str(update_hash), str(privatekey), blockchain_client_inst, blockchain_broadcaster=broadcaster_client_inst, testset=TESTSET )
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug("import <%s>" % name )
|
|
|
|
return resp
|
|
|
|
|
|
def jsonrpc_namespace_preorder( self, namespace_id, register_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.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
db = get_state_engine()
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
consensus_hash = db.get_current_consensus()
|
|
|
|
namespace_fee = price_namespace( namespace_id )
|
|
|
|
log.debug("Namespace '%s' will cost %s satoshis" % (namespace_id, namespace_fee))
|
|
|
|
try:
|
|
resp = namespace_preorder( str(namespace_id), str(register_addr), str(consensus_hash), str(privatekey), blockchain_client_inst, namespace_fee, testset=TESTSET )
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug("namespace_preorder <%s>" % (namespace_id))
|
|
return resp
|
|
|
|
|
|
def jsonrpc_namespace_reveal( self, namespace_id, register_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.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
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, testset=TESTSET )
|
|
except:
|
|
return json_traceback()
|
|
|
|
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 jsonrpc_namespace_ready( self, namespace_id, privatekey ):
|
|
"""
|
|
Declare that a namespace is open to accepting new names.
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
blockchain_client_inst = get_utxo_provider_client()
|
|
if blockchain_client_inst is None:
|
|
return {"error": "Failed to connect to blockchain UTXO provider"}
|
|
|
|
try:
|
|
resp = namespace_ready( str(namespace_id), str(privatekey), blockchain_client_inst, testset=TESTSET )
|
|
except:
|
|
return json_traceback()
|
|
|
|
log.debug("namespace_ready %s" % namespace_id )
|
|
return resp
|
|
|
|
|
|
def jsonrpc_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 is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
if len(name) > LENGTHS['blockchain_id_name']:
|
|
return {"error": "Name too long"}
|
|
|
|
ret = get_name_cost( name )
|
|
if ret is None:
|
|
return {"error": "Unknown/invalid namespace"}
|
|
|
|
return {"satoshis": int(math.ceil(ret))}
|
|
|
|
|
|
def jsonrpc_get_namespace_cost( self, namespace_id ):
|
|
"""
|
|
Return the cost of a given namespace, including fees.
|
|
Return value is in satoshis
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
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 jsonrpc_lookup_namespace( self, namespace_id ):
|
|
"""
|
|
Return the readied namespace with the given namespace_id
|
|
"""
|
|
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
db = get_state_engine()
|
|
ns = db.get_namespace( namespace_id )
|
|
if ns is None:
|
|
return {"error": "No such ready namespace"}
|
|
else:
|
|
return ns
|
|
|
|
|
|
def jsonrpc_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 jsonrpc_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 jsonrpc_get_consensus_at( self, block_id ):
|
|
"""
|
|
Return the consensus hash at a block number
|
|
"""
|
|
# are we doing our initial indexing?
|
|
if is_indexing():
|
|
return {"error": "Indexing blockchain"}
|
|
|
|
db = get_state_engine()
|
|
return db.get_consensus_at( block_id )
|
|
|
|
|
|
def run_indexer():
|
|
"""
|
|
Continuously reindex the blockchain, but as a subprocess.
|
|
"""
|
|
|
|
# set up this process
|
|
signal.signal( signal.SIGINT, die_handler_indexer )
|
|
signal.signal( signal.SIGQUIT, die_handler_indexer )
|
|
signal.signal( signal.SIGTERM, die_handler_indexer )
|
|
|
|
bitcoind_opts = get_bitcoin_opts()
|
|
|
|
_, last_block_id = get_index_range()
|
|
blockstore_state_engine = get_state_engine()
|
|
|
|
while True:
|
|
|
|
time.sleep( REINDEX_FREQUENCY )
|
|
virtualchain.sync_virtualchain( bitcoind_opts, last_block_id, blockstore_state_engine )
|
|
|
|
_, last_block_id = get_index_range()
|
|
|
|
return
|
|
|
|
|
|
def stop_server():
|
|
"""
|
|
Stop the blockstored server.
|
|
"""
|
|
global indexer_pid
|
|
|
|
# Quick hack to kill a background daemon
|
|
pid_file = get_pidfile_path()
|
|
|
|
try:
|
|
fin = open(pid_file, "r")
|
|
except Exception, e:
|
|
return
|
|
|
|
else:
|
|
pid_data = fin.read()
|
|
fin.close()
|
|
os.remove(pid_file)
|
|
|
|
pid = int(pid_data)
|
|
|
|
try:
|
|
os.kill(pid, signal.SIGKILL)
|
|
except Exception, e:
|
|
return
|
|
|
|
|
|
if indexer_pid is not None:
|
|
try:
|
|
os.kill(indexer_pid, signal.SIGTERM)
|
|
except Exception, e:
|
|
return
|
|
|
|
# stop building new state if we're in the middle of it
|
|
db = get_state_engine()
|
|
virtualchain.stop_sync_virtualchain( db )
|
|
|
|
set_indexing( False )
|
|
|
|
|
|
def get_indexing_lockfile():
|
|
"""
|
|
Return path to the indexing lockfile
|
|
"""
|
|
return os.path.join( virtualchain.get_working_dir(), "blockstore.indexing" )
|
|
|
|
|
|
def is_indexing():
|
|
"""
|
|
Is the blockstore 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 run_server( foreground=False ):
|
|
"""
|
|
Run the blockstored RPC server, optionally in the foreground.
|
|
"""
|
|
|
|
global indexer_pid
|
|
|
|
bt_opts = get_bitcoin_opts()
|
|
|
|
tac_file = get_tacfile_path()
|
|
access_log_file = get_logfile_path() + ".access"
|
|
indexer_log_file = get_logfile_path() + ".indexer"
|
|
pid_file = get_pidfile_path()
|
|
|
|
start_block, current_block = get_index_range()
|
|
|
|
argv0 = os.path.normpath( sys.argv[0] )
|
|
|
|
if os.path.exists("./%s" % argv0 ):
|
|
indexer_command = ("%s indexer" % (os.path.join( os.getcwd(), argv0))).split()
|
|
else:
|
|
# hope its in the $PATH
|
|
indexer_command = ("%s indexer" % argv0).split()
|
|
|
|
|
|
logfile = None
|
|
if not foreground:
|
|
|
|
api_server_command = ('twistd --pidfile=%s --logfile=%s -noy %s' % (pid_file,
|
|
access_log_file,
|
|
tac_file)).split()
|
|
|
|
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!
|
|
sys.exit(0)
|
|
|
|
else:
|
|
|
|
# error
|
|
sys.exit(1)
|
|
|
|
elif child_pid > 0:
|
|
|
|
# parent
|
|
# wait for child
|
|
pid, status = os.waitpid( child_pid, 0 )
|
|
sys.exit(status)
|
|
|
|
else:
|
|
|
|
# foreground
|
|
api_server_command = ('twistd --pidfile=%s -noy %s' % (pid_file, tac_file)).split()
|
|
|
|
|
|
# start API server
|
|
blockstored = subprocess.Popen( api_server_command, shell=False)
|
|
|
|
set_indexing( False )
|
|
|
|
if start_block != current_block:
|
|
# bring us up to speed
|
|
set_indexing( True )
|
|
|
|
blockstore_state_engine = get_state_engine()
|
|
virtualchain.sync_virtualchain( bt_opts, current_block, blockstore_state_engine )
|
|
|
|
set_indexing( False )
|
|
|
|
# fork the indexer
|
|
if foreground:
|
|
indexer = subprocess.Popen( indexer_command, shell=False )
|
|
else:
|
|
indexer = subprocess.Popen( indexer_command, shell=False, stdout=logfile, stderr=logfile )
|
|
|
|
indexer_pid = indexer.pid
|
|
|
|
# wait for the API server to die (we kill it with `blockstored stop`)
|
|
blockstored.wait()
|
|
|
|
# stop our indexer subprocess
|
|
indexer_pid = None
|
|
|
|
os.kill( indexer.pid, signal.SIGINT )
|
|
indexer.wait()
|
|
|
|
logfile.flush()
|
|
logfile.close()
|
|
|
|
# stop building new state if we're in the middle of it
|
|
db = get_state_engine()
|
|
virtualchain.stop_sync_virtualchain( db )
|
|
|
|
return blockstored.returncode
|
|
|
|
|
|
def setup( 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 blockstore_opts
|
|
global blockchain_client
|
|
global blockchain_broadcaster
|
|
global bitcoin_opts
|
|
global utxo_opts
|
|
global blockstore_opts
|
|
global dht_opts
|
|
|
|
# set up our implementation
|
|
virtualchain.setup_virtualchain( blockstore_state_engine )
|
|
|
|
# acquire configuration, and store it globally
|
|
blockstore_opts, bitcoin_opts, utxo_opts, dht_opts = configure( interactive=True )
|
|
|
|
# merge in command-line bitcoind options
|
|
config_file = virtualchain.get_config_filename()
|
|
|
|
arg_bitcoin_opts = None
|
|
argparser = None
|
|
|
|
if return_parser:
|
|
arg_bitcoin_opts, argparser = virtualchain.parse_bitcoind_args( return_parser=return_parser )
|
|
|
|
else:
|
|
arg_bitcoin_opts = virtualchain.parse_bitcoind_args( return_parser=return_parser )
|
|
|
|
# command-line overrides config file
|
|
for (k, v) in arg_bitcoin_opts.items():
|
|
bitcoin_opts[k] = v
|
|
|
|
# store options
|
|
set_bitcoin_opts( bitcoin_opts )
|
|
set_utxo_opts( utxo_opts )
|
|
|
|
if return_parser:
|
|
return argparser
|
|
else:
|
|
return None
|
|
|
|
|
|
def reconfigure():
|
|
"""
|
|
Reconfigure blockstored.
|
|
"""
|
|
configure( force=True )
|
|
print "Blockstore successfully reconfigured."
|
|
sys.exit(0)
|
|
|
|
|
|
def clean( confirm=True ):
|
|
"""
|
|
Remove blockstore's db, lastblock, and snapshot files.
|
|
Prompt for confirmation
|
|
"""
|
|
|
|
delete = False
|
|
exit_status = 0
|
|
|
|
if confirm:
|
|
warning = "WARNING: THIS WILL DELETE YOUR BLOCKSTORE DATABASE!\n"
|
|
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 run_blockstored():
|
|
"""
|
|
run blockstored
|
|
"""
|
|
|
|
argparser = setup( return_parser=True )
|
|
|
|
# get RPC server options
|
|
subparsers = argparser.add_subparsers(
|
|
dest='action', help='the action to be taken')
|
|
|
|
parser_server = subparsers.add_parser(
|
|
'start',
|
|
help='start the blockstored server')
|
|
parser_server.add_argument(
|
|
'--foreground', action='store_true',
|
|
help='start the blockstored server in foreground')
|
|
|
|
parser_server = subparsers.add_parser(
|
|
'stop',
|
|
help='stop the blockstored server')
|
|
|
|
parser_server = subparsers.add_parser(
|
|
'reconfigure',
|
|
help='reconfigure the blockstored server')
|
|
|
|
parser_server = subparsers.add_parser(
|
|
'clean',
|
|
help='remove all blockstore database information')
|
|
parser_server.add_argument(
|
|
'--force', action='store_true',
|
|
help='Do not confirm the request to delete.')
|
|
|
|
parser_server = subparsers.add_parser(
|
|
'indexer',
|
|
help='run blockstore indexer worker')
|
|
|
|
args, _ = argparser.parse_known_args()
|
|
|
|
log.debug( "bitcoin options: %s" % bitcoin_opts )
|
|
|
|
if args.action == 'start':
|
|
|
|
if os.path.exists( get_pidfile_path() ):
|
|
log.error("Blockstored appears to be running already. If not, please run '%s stop'" % (sys.argv[0]))
|
|
sys.exit(1)
|
|
|
|
if args.foreground:
|
|
|
|
log.info('Initializing blockstored server in foreground ...')
|
|
exit_status = run_server( foreground=True )
|
|
log.info("Service endpoint exited with status code %s" % exit_status )
|
|
|
|
else:
|
|
|
|
log.info('Starting blockstored server ...')
|
|
run_server()
|
|
|
|
elif args.action == 'stop':
|
|
stop_server()
|
|
|
|
elif args.action == 'reconfigure':
|
|
reconfigure()
|
|
|
|
elif args.action == 'clean':
|
|
clean( not args.force )
|
|
|
|
elif args.action == 'indexer':
|
|
run_indexer()
|
|
|
|
if __name__ == '__main__':
|
|
|
|
run_blockstored()
|