mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-06-12 15:48:54 +08:00
486 lines
14 KiB
Python
486 lines
14 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/>.
|
|
"""
|
|
|
|
# Hooks to the virtual chain's state engine that bind our namedb to the virtualchain package.
|
|
|
|
import os
|
|
from binascii import hexlify, unhexlify
|
|
import time
|
|
|
|
import pybitcoin
|
|
import traceback
|
|
import json
|
|
import simplejson
|
|
import threading
|
|
import copy
|
|
|
|
from .namedb import *
|
|
|
|
from ..config import *
|
|
from ..scripts import *
|
|
from ..operations import get_transfer_recipient_from_outputs, get_import_update_hash_from_outputs, get_registration_recipient_from_outputs, \
|
|
SERIALIZE_FIELDS
|
|
|
|
import virtualchain
|
|
log = virtualchain.get_logger("blockstack-log")
|
|
|
|
blockstack_db = None
|
|
last_load_time = 0
|
|
last_check_time = 0
|
|
reload_lock = threading.Lock()
|
|
|
|
|
|
def get_virtual_chain_name():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Get the name of the virtual chain we're building.
|
|
"""
|
|
return "blockstack-server"
|
|
|
|
|
|
def get_virtual_chain_version():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Get the version string for this virtual chain.
|
|
"""
|
|
return VERSION
|
|
|
|
|
|
def get_opcodes():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Get the list of opcodes we're looking for.
|
|
"""
|
|
return OPCODES
|
|
|
|
|
|
def get_op_processing_order():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Give a hint as to the order in which we process operations
|
|
"""
|
|
return OPCODES
|
|
|
|
|
|
def get_magic_bytes():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Get the magic byte sequence for our OP_RETURNs
|
|
"""
|
|
return MAGIC_BYTES
|
|
|
|
|
|
def get_first_block_id():
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Get the id of the first block to start indexing.
|
|
"""
|
|
start_block = FIRST_BLOCK_MAINNET
|
|
return start_block
|
|
|
|
|
|
def need_db_reload():
|
|
"""
|
|
Do we need to instantiate/reload the database?
|
|
"""
|
|
global blockstack_db
|
|
global last_load_time
|
|
global last_check_time
|
|
|
|
db_filename = virtualchain.get_db_filename()
|
|
|
|
sb = None
|
|
if os.path.exists(db_filename):
|
|
sb = os.stat(db_filename)
|
|
|
|
if blockstack_db is None:
|
|
# doesn't exist in RAM
|
|
log.debug("cache consistency: DB is not in RAM")
|
|
return True
|
|
|
|
if not os.path.exists(db_filename):
|
|
# doesn't exist on disk
|
|
log.debug("cache consistency: DB does not exist on disk")
|
|
return True
|
|
|
|
if sb is not None and sb.st_mtime != last_load_time:
|
|
# stale--new version exists on disk
|
|
log.debug("cache consistency: DB was modified; in-RAM copy is stale")
|
|
return True
|
|
|
|
if time.time() - last_check_time > 600:
|
|
# just for good measure--don't keep it around past the blocktime
|
|
log.debug("cache consistency: Blocktime has passed")
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_db_state( disposition=DISPOSITION_RO ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Callback to the virtual chain state engine.
|
|
Get a handle to our state engine implementation
|
|
(i.e. our name database).
|
|
|
|
Note that in this implementation, the database
|
|
handle returned will only support read-only operations by default.
|
|
NO COMMITS WILL BE ALLOWED.
|
|
"""
|
|
|
|
# make this usable even if we haven't explicitly configured virtualchain
|
|
impl = virtualchain.get_implementation()
|
|
if impl is None:
|
|
impl = sys.modules[__name__]
|
|
|
|
db_filename = virtualchain.get_db_filename(impl=impl)
|
|
lastblock_filename = virtualchain.get_lastblock_filename(impl=impl)
|
|
lastblock = None
|
|
firstcheck = True
|
|
|
|
for path in [db_filename, lastblock_filename]:
|
|
if os.path.exists( path ):
|
|
# have already created the db
|
|
firstcheck = False
|
|
|
|
if not firstcheck and not os.path.exists( lastblock_filename ):
|
|
# this can't ever happen
|
|
log.error("FATAL: no such file or directory: %s" % lastblock_filename )
|
|
os.abort()
|
|
|
|
# verify that it is well-formed, if it exists
|
|
elif os.path.exists( lastblock_filename ):
|
|
try:
|
|
with open(lastblock_filename, "r") as f:
|
|
lastblock = int( f.read().strip() )
|
|
|
|
except Exception, e:
|
|
# this can't ever happen
|
|
log.error("FATAL: failed to parse: %s" % lastblock_filename)
|
|
log.exception(e)
|
|
os.abort()
|
|
|
|
db_inst = BlockstackDB( db_filename, disposition )
|
|
|
|
return db_inst
|
|
|
|
|
|
def db_parse( block_id, txid, vtxindex, op, data, senders, inputs, outputs, fee, db_state=None ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Parse a blockstack operation from a transaction's nulldata (data) and a list of outputs, as well as
|
|
optionally the list of transaction's senders and the total fee paid. Use the operation-specific
|
|
extract_${OPCODE}() method to get the data, and make sure the operation-defined fields are all set.
|
|
|
|
Return None on error
|
|
|
|
NOTE: the transactions that our tools put have a single sender, and a single output address.
|
|
This is assumed by this code.
|
|
"""
|
|
|
|
# basic sanity checks
|
|
if len(senders) == 0:
|
|
raise Exception("No senders given")
|
|
|
|
# make sure each op has all the right fields defined
|
|
try:
|
|
opcode = op_get_opcode_name( op )
|
|
assert opcode is not None, "Unrecognized opcode '%s'" % op
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("Skipping unrecognized opcode")
|
|
return None
|
|
|
|
op_fee = get_burn_fee_from_outputs( outputs )
|
|
|
|
log.debug("PARSE %s at (%s, %s): %s" % (opcode, block_id, vtxindex, data.encode('hex')))
|
|
|
|
# get the data
|
|
op = None
|
|
try:
|
|
op = op_extract( opcode, data, senders, inputs, outputs, block_id, vtxindex, txid )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
op = None
|
|
|
|
if op is not None:
|
|
|
|
# propagate fees
|
|
if op_fee is not None:
|
|
op['op_fee'] = op_fee
|
|
|
|
# propagate tx data
|
|
op['vtxindex'] = int(vtxindex)
|
|
op['txid'] = str(txid)
|
|
|
|
else:
|
|
log.error("Unparseable op '%s'" % opcode)
|
|
|
|
return op
|
|
|
|
|
|
def check_mutate_fields( op, op_data ):
|
|
"""
|
|
Verify that all mutate fields are present.
|
|
"""
|
|
|
|
mutate_fields = op_get_mutate_fields( op )
|
|
assert mutate_fields is not None, "No mutate fields defined for %s" % op
|
|
|
|
missing = []
|
|
for field in mutate_fields:
|
|
if not op_data.has_key(field):
|
|
missing.append(field)
|
|
|
|
assert len(missing) == 0, "Missing mutation fields for %s: %s" % (op, ",".join(missing))
|
|
return True
|
|
|
|
|
|
def db_scan_block( block_id, op_list, db_state=None ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Given the block ID and the list of virtualchain operations in the block,
|
|
do block-level preprocessing:
|
|
* find the state-creation operations we will accept
|
|
* make sure there are no collisions.
|
|
"""
|
|
|
|
try:
|
|
assert db_state is not None, "BUG: no state given"
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: no state given")
|
|
os.abort()
|
|
|
|
checked_ops = []
|
|
for op_data in op_list:
|
|
|
|
try:
|
|
opcode = op_get_opcode_name( op_data['op'] )
|
|
assert opcode is not None, "BUG: unknown op '%s'" % op
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: invalid operation")
|
|
os.abort()
|
|
|
|
if opcode not in OPCODE_CREATION_OPS:
|
|
continue
|
|
|
|
# make sure there are no collisions:
|
|
# build up our collision table in db_state.
|
|
op_check( db_state, op_data, block_id, checked_ops )
|
|
checked_ops.append( op_data )
|
|
|
|
|
|
# get collision information for this block
|
|
collisions = db_state.find_collisions( checked_ops )
|
|
|
|
# reject all operations that will collide
|
|
db_state.put_collisions( block_id, collisions )
|
|
|
|
|
|
|
|
def db_check( block_id, new_ops, op, op_data, txid, vtxindex, checked_ops, db_state=None ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Given the block ID and a parsed operation, check to see if this is a *valid* operation.
|
|
Is this operation consistent with blockstack's rules?
|
|
|
|
checked_ops is a list of operations already checked by
|
|
this method for this block.
|
|
|
|
A name or namespace can be affected at most once per block. If it is
|
|
affected more than once, then the opcode priority rules take effect, and
|
|
the lower priority opcodes are rejected.
|
|
|
|
Return True if it's valid; False if not.
|
|
"""
|
|
|
|
accept = True
|
|
|
|
if db_state is not None:
|
|
|
|
try:
|
|
assert 'txid' in op_data, "Missing txid from op"
|
|
assert 'vtxindex' in op_data, "Missing vtxindex from op"
|
|
opcode = op_get_opcode_name( op )
|
|
assert opcode is not None, "BUG: unknown op '%s'" % op
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: invalid operation")
|
|
os.abort()
|
|
|
|
log.debug("CHECK %s at (%s, %s)" % (opcode, block_id, vtxindex))
|
|
rc = op_check( db_state, op_data, block_id, checked_ops )
|
|
if rc:
|
|
|
|
try:
|
|
opcode = op_data.get('opcode', None)
|
|
assert opcode is not None, "BUG: op_check did not set an opcode"
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: no opcode set")
|
|
os.abort()
|
|
|
|
# verify that all mutate fields are present
|
|
rc = check_mutate_fields( opcode, op_data )
|
|
if not rc:
|
|
log.error("FATAL: bug in '%s' check() method did not return all mutate fields" % opcode)
|
|
os.abort()
|
|
|
|
else:
|
|
accept = False
|
|
|
|
return accept
|
|
|
|
|
|
def db_commit( block_id, op, op_data, txid, vtxindex, db_state=None ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Advance the state of the state engine: get a list of all
|
|
externally visible state transitions.
|
|
|
|
Given a block ID and checked opcode, record it as
|
|
part of the database. This does *not* need to write
|
|
the data to persistent storage, since save() will be
|
|
called once per block processed.
|
|
|
|
Returns one or more new name operations on success, which will
|
|
be fed into virtualchain to translate into a string
|
|
to be used to generate this block's consensus hash.
|
|
"""
|
|
|
|
if db_state is not None:
|
|
if op_data is not None:
|
|
|
|
try:
|
|
assert 'txid' in op_data, "BUG: No txid given"
|
|
assert 'vtxindex' in op_data, "BUG: No vtxindex given"
|
|
assert op_data['txid'] == txid, "BUG: txid mismatch"
|
|
assert op_data['vtxindex'] == vtxindex, "BUG: vtxindex mismatch"
|
|
# opcode = op_get_opcode_name( op_data['op'] )
|
|
opcode = op_data.get('opcode', None)
|
|
assert opcode in OPCODE_PREORDER_OPS + OPCODE_CREATION_OPS + OPCODE_TRANSITION_OPS + OPCODE_STATELESS_OPS, \
|
|
"BUG: uncategorized opcode '%s'" % opcode
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to commit operation")
|
|
os.abort()
|
|
|
|
if opcode in OPCODE_STATELESS_OPS:
|
|
# state-less operation
|
|
return []
|
|
|
|
else:
|
|
op_seq = db_state.commit_operation( op_data, block_id )
|
|
return op_seq
|
|
|
|
else:
|
|
# final commit for this block
|
|
try:
|
|
db_state.commit_finished( block_id )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to commit at block %s" % block_id )
|
|
os.abort()
|
|
|
|
return None
|
|
|
|
else:
|
|
log.error("FATAL: no state engine given")
|
|
os.abort()
|
|
|
|
|
|
|
|
def db_save( block_id, consensus_hash, pending_ops, filename, db_state=None ):
|
|
"""
|
|
(required by virtualchain state engine)
|
|
|
|
Save all persistent state to stable storage.
|
|
Clear out expired names in the process.
|
|
Called once per block.
|
|
|
|
Return True on success
|
|
Return False on failure.
|
|
"""
|
|
|
|
if db_state is not None:
|
|
|
|
try:
|
|
# pre-calculate the ops hash for SNV
|
|
ops_hash = BlockstackDB.calculate_block_ops_hash( db_state, block_id )
|
|
db_state.store_block_ops_hash( block_id, ops_hash )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to calculate ops hash at block %s" % block_id )
|
|
os.abort()
|
|
|
|
try:
|
|
# flush the database
|
|
db_state.commit_finished( block_id )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: failed to commit at block %s" % block_id )
|
|
os.abort()
|
|
|
|
return True
|
|
|
|
else:
|
|
log.error("FATAL: no state engine given")
|
|
os.abort()
|
|
|
|
|
|
def sync_blockchain( bt_opts, last_block, expected_snapshots={}, **virtualchain_args ):
|
|
"""
|
|
synchronize state with the blockchain.
|
|
build up the next blockstack_db
|
|
"""
|
|
|
|
# make this usable even if we haven't explicitly configured virtualchain
|
|
impl = sys.modules[__name__]
|
|
if virtualchain.get_implementation() is not None:
|
|
impl = None
|
|
|
|
log.info("Synchronizing database up to block %s" % last_block)
|
|
|
|
db_filename = virtualchain.get_db_filename(impl=impl)
|
|
|
|
# NOTE: this is the only place where a read-write handle should be created,
|
|
# since this is the only place where the db should be modified.
|
|
new_db = BlockstackDB.borrow_readwrite_instance( db_filename, last_block, expected_snapshots=expected_snapshots )
|
|
virtualchain.sync_virtualchain( bt_opts, last_block, new_db, expected_snapshots=expected_snapshots, **virtualchain_args )
|
|
BlockstackDB.release_readwrite_instance( new_db, last_block )
|
|
|