mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-10 08:50:49 +08:00
427 lines
15 KiB
Python
427 lines
15 KiB
Python
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack
|
|
~~~~~
|
|
copyright: (c) 2014 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2015 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 sys
|
|
|
|
from ..config import OPCODE_CREATION_OPS, OPCODE_TRANSITION_OPS, op_get_opcode_name
|
|
|
|
import virtualchain
|
|
log = virtualchain.get_logger("blockstack-server")
|
|
|
|
|
|
# fields that *must* be present
|
|
CONSENSUS_FIELDS_REQUIRED = [
|
|
'op',
|
|
'txid',
|
|
'vtxindex'
|
|
]
|
|
|
|
# all fields common to a name record
|
|
# NOTE: this order must be preserved for all eternity
|
|
NAMEREC_FIELDS = [
|
|
'name', # the name itself
|
|
'value_hash', # the hash of the name's associated profile
|
|
'sender', # the scriptPubKey hex that owns this name (identifies ownership)
|
|
'sender_pubkey', # (OPTIONAL) the public key
|
|
'address', # the address of the sender
|
|
|
|
'block_number', # the block number when this name record was created (preordered for the first time)
|
|
'preorder_block_number', # the block number when this name was last preordered
|
|
'first_registered', # the block number when this name was registered by the current owner
|
|
'last_renewed', # the block number when this name was renewed by the current owner
|
|
'revoked', # whether or not the name is revoked
|
|
|
|
'op', # byte sequence describing the last operation to affect this name
|
|
'txid', # the ID of the last transaction to affect this name
|
|
'vtxindex', # the index in the block of the transaction.
|
|
'op_fee', # the value of the last Blockstack-specific burn fee paid for this name (i.e. from preorder or renew)
|
|
|
|
'importer', # (OPTIONAL) if this name was imported, this is the importer's scriptPubKey hex
|
|
'importer_address', # (OPTIONAL) if this name was imported, this is the importer's address
|
|
]
|
|
|
|
# common set of fields that will get changed when applying an operation
|
|
NAMEREC_MUTATE_FIELDS = [
|
|
'txid',
|
|
'vtxindex',
|
|
'op'
|
|
]
|
|
|
|
# common set of fields that will need to be backed up to a name's history when applying an operation
|
|
NAMEREC_BACKUP_FIELDS = NAMEREC_MUTATE_FIELDS[:]
|
|
|
|
NAMEREC_NAME_BACKUP_FIELDS = [
|
|
'transfer_send_block_id'
|
|
]
|
|
|
|
# fields that are not fed into the consensus hash, but are used to generate
|
|
# consensus-affecting fields. They must be present when restoring a prior
|
|
# version of a name.
|
|
NAMEREC_INDIRECT_CONSENSUS_FIELDS = [
|
|
'opcode',
|
|
'transfer_send_block_id',
|
|
'burn_address'
|
|
]
|
|
|
|
def state_create_invariant_tags():
|
|
"""
|
|
Get a list of state-create invariant tags.
|
|
"""
|
|
return [
|
|
'__preorder__',
|
|
'__prior_history__',
|
|
'__table__',
|
|
'__history_id_key__',
|
|
'__state_create__'
|
|
]
|
|
|
|
|
|
# check for collisions
|
|
def state_check_collisions( state_engine, nameop, history_id_key, block_id, checked_ops, collision_checker ):
|
|
"""
|
|
See that there are no state-creating or state-preordering collisions at this block, for this history ID.
|
|
Return True if collided; False if not
|
|
"""
|
|
|
|
# verify no collisions against already-accepted names
|
|
collision_check = getattr( state_engine, collision_checker, None )
|
|
try:
|
|
assert collision_check is not None, "Collision-checker '%s' not defined" % collision_checker
|
|
assert hasattr( collision_check, "__call__" ), "Collision-checker '%s' is not callable" % collision_checker
|
|
assert history_id_key in nameop.keys(), "History ID key '%s' not in name operation" % (history_id_key)
|
|
assert 'op' in nameop.keys(), "BUG: no op in nameop"
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: incorrect state_create() decorator")
|
|
sys.exit(1)
|
|
|
|
rc = collision_check( nameop[history_id_key], block_id, checked_ops )
|
|
return rc
|
|
|
|
|
|
# sanity check decorator for state-preordering operations
|
|
def state_preorder(collision_checker):
|
|
"""
|
|
Decorator for the check() method on a state-preordering operation.
|
|
Make sure that there are no duplicate preorders anywhere--either in this
|
|
block, or in any previous blocks.
|
|
"""
|
|
def wrap( check ):
|
|
def wrapped_check( state_engine, nameop, block_id, checked_ops ):
|
|
rc = check( state_engine, nameop, block_id, checked_ops )
|
|
if rc:
|
|
# verify no duplicates
|
|
history_id_key = "preorder_hash"
|
|
rc = state_check_collisions( state_engine, nameop, history_id_key, block_id, checked_ops, collision_checker )
|
|
if rc:
|
|
log.debug("COLLISION on %s '%s'" % (history_id_key, nameop[history_id_key]))
|
|
rc = False
|
|
else:
|
|
# no collision
|
|
rc = True
|
|
|
|
return rc
|
|
return wrapped_check
|
|
return wrap
|
|
|
|
|
|
# sanity check decorator for state-creating operations
|
|
def state_create(history_id_key, table_name, collision_checker, always_set=[]):
|
|
"""
|
|
Decorator for the check() method on state-creating operations.
|
|
Makes sure that:
|
|
* there is a __preorder__ field set, which contains the state-creating operation's associated preorder
|
|
* there is a __prior_history__ field set, which contains the state-creating operation's associated prior state history
|
|
* there is a __table__ field set, which contains the table into which to insert this state into
|
|
* there is a __history_id_key__ field set, which identifies the table's primary key name
|
|
* there are no unexpired, duplicate instances of this state with this history id.
|
|
(i.e. if we're preordering a name that had previously expired, we need to preserve its history)
|
|
"""
|
|
|
|
def wrap( check ):
|
|
def wrapped_check( state_engine, nameop, block_id, checked_ops ):
|
|
rc = check( state_engine, nameop, block_id, checked_ops )
|
|
|
|
# succeeded, and still a state-creating operation?
|
|
if rc and op_get_opcode_name( nameop['op'] ) in OPCODE_CREATION_OPS:
|
|
|
|
# ensure that there's now a __preorder__ and a __prior_history__
|
|
try:
|
|
assert '__preorder__' in nameop.keys(), "Missing __preorder__"
|
|
assert '__prior_history__' in nameop.keys(), "Missing __prior_history__"
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: missing fields")
|
|
sys.exit(1)
|
|
|
|
# propagate __table__ and __history_id_key__
|
|
nameop['__table__'] = table_name
|
|
nameop['__history_id_key__'] = history_id_key
|
|
nameop['__state_create__'] = True
|
|
nameop['__always_set__'] = always_set
|
|
|
|
# sanity check
|
|
invariant_tags = state_create_invariant_tags()
|
|
for tag in invariant_tags:
|
|
assert tag in nameop, "BUG: missing invariant tag '%s'" % tag
|
|
|
|
# verify no duplicates
|
|
rc = state_check_collisions( state_engine, nameop, history_id_key, block_id, checked_ops, collision_checker )
|
|
if rc:
|
|
# this is a duplicate!
|
|
log.debug("COLLISION on %s '%s'" % (history_id_key, nameop[history_id_key]))
|
|
rc = False
|
|
else:
|
|
# no collision
|
|
rc = True
|
|
|
|
return rc
|
|
return wrapped_check
|
|
return wrap
|
|
|
|
|
|
def state_transition_invariant_tags():
|
|
"""
|
|
Get a list of possible state-transition invariant tags
|
|
"""
|
|
return [
|
|
'__table__',
|
|
'__history_id_key__',
|
|
'__state_transition__',
|
|
'__always_set__'
|
|
]
|
|
|
|
|
|
# sanity check decorator for state-transition operations
|
|
def state_transition(history_id_key, table_name, always_set=[]):
|
|
"""
|
|
Decorator for the check() method on state-transition operations.
|
|
Make sure that:
|
|
* there is a __table__ field set, which names the table in which this record is stored.
|
|
* there is a __history_id_key__ field set, which identifies the table record's primary key.
|
|
|
|
Any fields named in @always_set will always be set when the transition is applied.
|
|
That is, fields set here *must* be set on transition, and *will* be set in the database, even if
|
|
they have prior values in the affected name record that might constrain which rows to update.
|
|
"""
|
|
|
|
def wrap( check ):
|
|
def wrapped_check( state_engine, nameop, block_id, checked_ops ):
|
|
rc = check( state_engine, nameop, block_id, checked_ops )
|
|
if rc:
|
|
# put fields in place
|
|
nameop['__table__'] = table_name
|
|
nameop['__history_id_key__'] = history_id_key
|
|
nameop['__state_transition__'] = True
|
|
nameop['__always_set__'] = always_set
|
|
|
|
# sanity check
|
|
invariant_tags = state_transition_invariant_tags()
|
|
for tag in invariant_tags:
|
|
assert tag in nameop, "BUG: missing invariant tag '%s'" % tag
|
|
|
|
return rc
|
|
return wrapped_check
|
|
return wrap
|
|
|
|
|
|
def get_state_invariant_tags():
|
|
"""
|
|
Get the set of state invariant tags for a given opcode
|
|
"""
|
|
return list(set( state_create_invariant_tags() + state_transition_invariant_tags() ))
|
|
|
|
|
|
def state_create_put_preorder( nameop, preorder ):
|
|
"""
|
|
Call this in a @state_create-decorated method.
|
|
"""
|
|
nameop['__preorder__'] = preorder
|
|
|
|
|
|
def state_create_put_prior_history( nameop, prior_history_rec ):
|
|
"""
|
|
Call this in a @state_create-decorated method.
|
|
"""
|
|
nameop['__prior_history__'] = prior_history_rec
|
|
|
|
|
|
def state_create_is_valid( nameop ):
|
|
"""
|
|
Is a nameop a valid state-preorder operation?
|
|
"""
|
|
assert '__state_create__' in nameop, "Not tagged with @state_create"
|
|
assert nameop['__state_create__'], "BUG: tagged False by @state_create"
|
|
assert '__preorder__' in nameop, "No preorder"
|
|
assert '__prior_history__' in nameop, "No prior history"
|
|
assert '__table__' in nameop, "No table given"
|
|
assert '__history_id_key__' in nameop, "No history ID key given"
|
|
assert nameop['__history_id_key__'] in nameop, "No history ID given"
|
|
assert '__always_set__' in nameop, "No always-set fields given"
|
|
|
|
return True
|
|
|
|
|
|
def state_create_get_preorder( nameop ):
|
|
"""
|
|
Get the preorder record for a state-creating operation
|
|
"""
|
|
return nameop['__preorder__']
|
|
|
|
|
|
def state_create_get_prior_history( nameop ):
|
|
"""
|
|
Get the prior history for a state-creating operation
|
|
"""
|
|
return nameop['__prior_history__']
|
|
|
|
|
|
def state_create_get_table( nameop ):
|
|
"""
|
|
Get the table of a state-creating operation
|
|
"""
|
|
return nameop['__table__']
|
|
|
|
|
|
def state_create_get_history_id_key( nameop ):
|
|
"""
|
|
Get the key to the history ID of a state-create name operation
|
|
"""
|
|
return nameop['__history_id_key__']
|
|
|
|
|
|
def state_create_get_always_set( nameop ):
|
|
"""
|
|
Get thie list of fields we will always set on create.
|
|
"""
|
|
return nameop['__always_set__']
|
|
|
|
|
|
def state_transition_is_valid( nameop ):
|
|
"""
|
|
Is this a valid state transition?
|
|
"""
|
|
assert '__state_transition__' in nameop, "Not tagged with @state_transition"
|
|
assert nameop['__state_transition__'], "BUG: @state_transition tagged False"
|
|
assert '__history_id_key__' in nameop, "Missing __history_id_key__"
|
|
history_id_key = nameop['__history_id_key__']
|
|
assert history_id_key in ["name", "namespace_id"], "Invalid history ID key '%s'" % history_id_key
|
|
assert '__table__' in nameop, "Missing __table__"
|
|
assert '__always_set__' in nameop, "No always-set fields given"
|
|
|
|
return True
|
|
|
|
|
|
def state_transition_get_table( nameop ):
|
|
"""
|
|
Get the table of a state-transition operation
|
|
"""
|
|
return nameop['__table__']
|
|
|
|
|
|
def state_transition_get_history_id_key( nameop ):
|
|
"""
|
|
Get the key of the history ID of a state-transition name operation
|
|
"""
|
|
return nameop['__history_id_key__']
|
|
|
|
|
|
def state_transition_get_always_set( nameop ):
|
|
"""
|
|
Get thie list of fields we will always set on state transition
|
|
"""
|
|
return nameop['__always_set__']
|
|
|
|
|
|
def prior_history_create( op_data, old_rec, block_number, state_engine, extra_backup_fields=[] ):
|
|
"""
|
|
Given a state-creating operation and the older version of
|
|
of said state (possibly expired, or previously-imported, etc.),
|
|
create a history for it.
|
|
"""
|
|
|
|
from ..operations import SERIALIZE_FIELDS, op_snv_consensus_extra
|
|
|
|
opcode = op_get_opcode_name( op_data['op'] )
|
|
try:
|
|
serialize_fields = SERIALIZE_FIELDS.get( opcode, None )
|
|
assert serialize_fields is not None, "Undefined opcode '%s'" % opcode
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: missing fields")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
assert 'history' in old_rec.keys()
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("FATAL: missing prior hitsory")
|
|
sys.exit(1)
|
|
|
|
hist = {}
|
|
for field in list(set(serialize_fields + extra_backup_fields)):
|
|
hist[field] = old_rec.get(field, None)
|
|
|
|
hist['history'] = old_rec['history']
|
|
state_engine.add_all_snv_consensus_values( op_get_opcode_name(hist['op']), hist, block_number )
|
|
hist['history_snapshot'] = True
|
|
|
|
del hist['history']
|
|
prior_history = {
|
|
block_number: [hist]
|
|
}
|
|
|
|
return prior_history
|
|
|
|
|
|
def prior_history_is_valid( prior_history_rec ):
|
|
"""
|
|
Is the given dict a valid prior history,
|
|
created by "prior_history"?
|
|
"""
|
|
|
|
assert type(prior_history_rec) == dict, "Not a dict"
|
|
assert len(prior_history_rec.keys()) == 1, "Invalid number of history blocks"
|
|
assert len(prior_history_rec[ prior_history_rec.keys()[0] ]) == 1, "Invalid number of history snapshots"
|
|
return True
|
|
|
|
|
|
def prior_history_block_number( prior_history_rec ):
|
|
"""
|
|
Get the block number of a prior history.
|
|
"""
|
|
assert prior_history_is_valid( prior_history_rec )
|
|
return prior_history_rec.keys()[0]
|
|
|
|
|
|
import namedb
|
|
import virtualchain_hooks
|
|
|
|
from .namedb import BlockstackDB, DISPOSITION_RO, DISPOSITION_RW
|
|
|
|
# this module is suitable to be a virtualchain state engine implementation
|
|
from .virtualchain_hooks import *
|
|
|
|
from db import sqlite3_find_tool, sqlite3_backup
|
|
|