mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-04-23 19:31:00 +08:00
Merge branch 'master' into develop
This commit is contained in:
@@ -69,7 +69,8 @@ from lib.schemas import GENESIS_BLOCK_SCHEMA
|
||||
from lib.rpc import BlockstackAPIEndpoint
|
||||
from lib.subdomains import (subdomains_init, SubdomainNotFound, get_subdomain_info, get_subdomain_history,
|
||||
get_DID_subdomain, get_subdomains_owned_by_address, get_subdomain_DID_info,
|
||||
get_all_subdomains, get_subdomains_count, get_subdomain_resolver, is_subdomain_zonefile_hash)
|
||||
get_all_subdomains, get_subdomains_count, get_subdomain_resolver, is_subdomain_zonefile_hash,
|
||||
get_subdomain_ops_at_txid)
|
||||
|
||||
from lib.scripts import address_as_b58, is_c32_address
|
||||
from lib.c32 import c32ToB58
|
||||
@@ -688,6 +689,20 @@ class BlockstackdRPC(BoundedThreadingMixIn, SimpleXMLRPCServer):
|
||||
if 'error' in res:
|
||||
return {'error': res['error'], 'http_status': 404}
|
||||
|
||||
# also get a DID
|
||||
did_info = None
|
||||
did = None
|
||||
if check_name(name):
|
||||
did_info = self.get_name_DID_info(name)
|
||||
elif check_subdomain(name):
|
||||
did_info = self.get_subdomain_DID_info(name)
|
||||
else:
|
||||
return {'error': 'Invalid name or subdomain', 'http_status': 400}
|
||||
|
||||
if did_info is not None:
|
||||
did = make_DID(did_info['name_type'], did_info['address'], did_info['index'])
|
||||
res['record']['did'] = did
|
||||
|
||||
return self.success_response({'record': res['record']})
|
||||
|
||||
|
||||
@@ -1587,6 +1602,19 @@ class BlockstackdRPC(BoundedThreadingMixIn, SimpleXMLRPCServer):
|
||||
return self.success_response( {'names': res} )
|
||||
|
||||
|
||||
def rpc_get_subdomain_ops_at_txid(self, txid, **con_info):
|
||||
"""
|
||||
Return the list of subdomain operations accepted within a given txid.
|
||||
Return {'status': True, 'subdomain_ops': [{...}]} on success
|
||||
Return {'error': ...} on error
|
||||
"""
|
||||
if not check_string(txid, min_length=64, max_length=64, pattern='^[0-9a-fA-F]{64}$'):
|
||||
return {'error': 'Not a valid txid', 'http_status': 400}
|
||||
|
||||
subdomain_ops = get_subdomain_ops_at_txid(txid)
|
||||
return self.success_response( {'subdomain_ops': subdomain_ops} )
|
||||
|
||||
|
||||
def rpc_get_consensus_at( self, block_id, **con_info ):
|
||||
"""
|
||||
Return the consensus hash at a block number.
|
||||
|
||||
@@ -2332,7 +2332,7 @@ def get_subdomains_owned_by_address(address, proxy=None, hostport=None):
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(e)
|
||||
|
||||
resp = {'error': 'Server response included an invalid subdomain'}
|
||||
resp = {'error': 'Server response included an invalid subdomain', 'http_status': 500}
|
||||
return resp
|
||||
|
||||
except socket.timeout:
|
||||
@@ -2356,6 +2356,79 @@ def get_subdomains_owned_by_address(address, proxy=None, hostport=None):
|
||||
return resp['subdomains']
|
||||
|
||||
|
||||
def get_subdomain_ops_at_txid(txid, proxy=None, hostport=None):
|
||||
"""
|
||||
Get the list of subdomain operations added by a txid
|
||||
Returns the list of operations ([{...}]) on success
|
||||
Returns {'error': ...} on failure
|
||||
"""
|
||||
assert proxy or hostport, 'Need proxy or hostport'
|
||||
if proxy is None:
|
||||
proxy = connect_hostport(hostport)
|
||||
|
||||
subdomain_ops_schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'subdomain_ops': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': OP_HISTORY_SCHEMA['properties'],
|
||||
'required': SUBDOMAIN_HISTORY_REQUIRED,
|
||||
},
|
||||
},
|
||||
},
|
||||
'required': ['subdomain_ops'],
|
||||
}
|
||||
|
||||
schema = json_response_schema(subdomain_ops_schema)
|
||||
|
||||
resp = {}
|
||||
try:
|
||||
resp = proxy.get_subdomain_ops_at_txid(txid)
|
||||
resp = json_validate(schema, resp)
|
||||
if json_is_error(resp):
|
||||
return resp
|
||||
|
||||
# names must be valid
|
||||
for op in resp['subdomain_ops']:
|
||||
assert is_subdomain(str(op['fully_qualified_subdomain'])), ('Invalid subdomain "{}"'.format(op['fully_qualified_subdomain']))
|
||||
|
||||
except ValidationError as ve:
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(ve)
|
||||
|
||||
resp = {'error': 'Server response did not match expected schema. You are likely communicating with an out-of-date Blockstack node.', 'http_status': 502}
|
||||
return resp
|
||||
|
||||
except AssertionError as e:
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(e)
|
||||
|
||||
resp = {'error': 'Server response included an invalid subdomain', 'http_status': 500}
|
||||
return resp
|
||||
|
||||
except socket.timeout:
|
||||
log.error("Connection timed out")
|
||||
resp = {'error': 'Connection to remote host timed out.', 'http_status': 503}
|
||||
return resp
|
||||
|
||||
except socket.error as se:
|
||||
log.error("Connection error {}".format(se.errno))
|
||||
resp = {'error': 'Connection to remote host failed.', 'http_status': 502}
|
||||
return resp
|
||||
|
||||
except Exception as ee:
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(ee)
|
||||
|
||||
log.error("Caught exception while connecting to Blockstack node: {}".format(ee))
|
||||
resp = {'error': 'Failed to contact Blockstack node. Try again with `--debug`.', 'http_status': 500}
|
||||
return resp
|
||||
|
||||
return resp['subdomain_ops']
|
||||
|
||||
|
||||
def get_name_DID(name, proxy=None, hostport=None):
|
||||
"""
|
||||
Get the DID for a name or subdomain
|
||||
@@ -3586,7 +3659,7 @@ def resolve_DID(did, hostport=None, proxy=None):
|
||||
4. fetch and authenticate the JWT at each URL (abort if there are none)
|
||||
5. extract the public key from the JWT and return that.
|
||||
|
||||
Return {'public_key': ...} on success
|
||||
Return {'public_key': ..., 'document': ...} on success
|
||||
Return {'error': ...} on error
|
||||
"""
|
||||
assert hostport or proxy, 'Need hostport or proxy'
|
||||
@@ -3628,9 +3701,41 @@ def resolve_DID(did, hostport=None, proxy=None):
|
||||
if not jwt:
|
||||
continue
|
||||
|
||||
if 'payload' not in jwt:
|
||||
log.error('Invalid JWT at {}: no payload'.format(url))
|
||||
continue
|
||||
|
||||
if 'issuer' not in jwt['payload']:
|
||||
log.error('Invalid JWT at {}: no issuer'.format(url))
|
||||
continue
|
||||
|
||||
if 'publicKey' not in jwt['payload']['issuer']:
|
||||
log.error('Invalid JWT at {}: no public key'.format(url))
|
||||
continue
|
||||
|
||||
if 'claim' not in jwt['payload']:
|
||||
log.error('Invalid JWT at {}: no claim'.format(url))
|
||||
continue
|
||||
|
||||
if not isinstance(jwt['payload'], dict):
|
||||
log.error('Invalid JWT at {}: claim is malformed'.format(url))
|
||||
continue
|
||||
|
||||
# found!
|
||||
public_key = str(jwt['payload']['issuer']['publicKey'])
|
||||
return {'public_key': public_key}
|
||||
document = jwt['payload']['claim']
|
||||
|
||||
# make sure it's a well-formed DID
|
||||
document['@context'] = 'https://w3id.org/did/v1'
|
||||
document['publicKey'] = [
|
||||
{
|
||||
'id': did,
|
||||
'type': 'secp256k1',
|
||||
'publicKeyHex': public_key
|
||||
}
|
||||
]
|
||||
|
||||
return {'public_key': public_key, 'document': document}
|
||||
|
||||
log.error("No zone file URLs resolved to a JWT with the public key whose address is {}".format(did_rec['address']))
|
||||
return {'error': 'No public key found for the given DID', 'http_status': 404}
|
||||
|
||||
@@ -275,7 +275,11 @@ class BlockstackDB(virtualchain.StateEngine):
|
||||
"""
|
||||
Get the paths to the relevant db files to back up
|
||||
"""
|
||||
return super(BlockstackDB, cls).get_state_paths(impl, working_dir) + [os.path.join(working_dir, 'atlas.db'), os.path.join(working_dir, 'subdomains.db')]
|
||||
return super(BlockstackDB, cls).get_state_paths(impl, working_dir) + [
|
||||
os.path.join(working_dir, 'atlas.db'),
|
||||
os.path.join(working_dir, 'subdomains.db'),
|
||||
os.path.join(working_dir, 'subdomains.db.queue')
|
||||
]
|
||||
|
||||
|
||||
def get_db_path( self ):
|
||||
@@ -313,9 +317,11 @@ class BlockstackDB(virtualchain.StateEngine):
|
||||
snapshots_path = os.path.join(dirpath, os.path.basename(virtualchain.get_snapshots_filename(virtualchain_hooks, self.working_dir)))
|
||||
atlas_path = os.path.join(dirpath, 'atlas.db')
|
||||
subdomain_path = os.path.join(dirpath, 'subdomains.db')
|
||||
subdomain_queue_path = os.path.join(dirpath, 'subdomains.db.queue')
|
||||
|
||||
src_atlas_path = os.path.join(self.working_dir, 'atlas.db')
|
||||
src_subdomain_path = os.path.join(self.working_dir, 'subdomains.db')
|
||||
src_subdomain_queue_path = os.path.join(self.working_dir, 'subdomains.db.queue')
|
||||
|
||||
virtualchain.sqlite3_backup(self.get_db_path(), db_path)
|
||||
virtualchain.sqlite3_backup(virtualchain.get_snapshots_filename(virtualchain_hooks, self.working_dir), snapshots_path)
|
||||
@@ -325,6 +331,9 @@ class BlockstackDB(virtualchain.StateEngine):
|
||||
|
||||
if os.path.exists(src_subdomain_path):
|
||||
virtualchain.sqlite3_backup(src_subdomain_path, subdomain_path)
|
||||
|
||||
if os.path.exists(src_subdomain_queue_path):
|
||||
virtualchain.sqlite3_backup(src_subdomain_queue_path, subdomain_queue_path)
|
||||
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -47,7 +47,7 @@ import scripts as blockstackd_scripts
|
||||
from scripts import check_name, check_namespace, check_token_type, check_subdomain, check_block, check_offset, \
|
||||
check_count, check_string, check_address, check_account_address
|
||||
|
||||
from util import BoundedThreadingMixIn
|
||||
from util import BoundedThreadingMixIn, parse_DID
|
||||
|
||||
import storage
|
||||
|
||||
@@ -60,7 +60,7 @@ from virtualchain import AuthServiceProxy, JSONRPCException
|
||||
|
||||
import blockstack_zones
|
||||
|
||||
from schemas import OP_BASE64_EMPTY_PATTERN, OP_ZONEFILE_HASH_PATTERN
|
||||
from schemas import OP_BASE64_EMPTY_PATTERN, OP_ZONEFILE_HASH_PATTERN, OP_BASE58CHECK_CLASS
|
||||
|
||||
log = virtualchain.get_logger()
|
||||
|
||||
@@ -491,11 +491,9 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
res = blockstackd_client.get_all_names(offset, count, include_expired=include_expired, hostport=blockstackd_url)
|
||||
if json_is_error(res):
|
||||
log.error("Failed to list all names (offset={}, count={}): {}".format(offset, count, res['error']))
|
||||
self._reply_json({'error': 'Failed to list all names'}, status_code=res.get('http_status', 502))
|
||||
return
|
||||
return self._reply_json({'error': 'Failed to list all names'}, status_code=res.get('http_status', 502))
|
||||
|
||||
self._reply_json(res)
|
||||
return
|
||||
return self._reply_json(res)
|
||||
|
||||
|
||||
def GET_subdomains( self, path_info ):
|
||||
@@ -527,11 +525,29 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
if json_is_error(res):
|
||||
log.error("Failed to list all subdomains (offset={}, count={}): {}".format(offset, count, res['error']))
|
||||
self._reply_json({'error': 'Failed to list all names'}, status_code=406)
|
||||
return
|
||||
return self._reply_json({'error': 'Failed to list all names'}, status_code=res.get('http_status', 406))
|
||||
|
||||
self._reply_json(res)
|
||||
return
|
||||
return self._reply_json(res)
|
||||
|
||||
|
||||
def GET_subdomain_ops(self, path_info, txid):
|
||||
"""
|
||||
Get all subdomain operations processed in a given transaction.
|
||||
Returns the list of subdomains on success (can be empty)
|
||||
Returns 502 on failure to get subdomains
|
||||
"""
|
||||
blockstackd_url = get_blockstackd_url()
|
||||
subdomain_ops = None
|
||||
try:
|
||||
subdomain_ops = blockstackd_client.get_subdomain_ops_at_txid(txid, hostport=blockstackd_url)
|
||||
except ValueError:
|
||||
return self._reply_json({'error': 'Invalid argument: not a well-formed txid'}, status_code=400)
|
||||
|
||||
if json_is_error(subdomain_ops):
|
||||
log.error('Failed to get subdomain operations at {}: {}'.format(txid, subdomain_ops['error']))
|
||||
return self._reply_json({'error': 'Failed to get subdomain operations'}, status_code=subdomain_ops.get('http_status', 500))
|
||||
|
||||
return self._reply_json(subdomain_ops)
|
||||
|
||||
|
||||
def GET_name_info( self, path_info, name ):
|
||||
@@ -604,6 +620,7 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
'address': name_rec['address'],
|
||||
'blockchain': 'bitcoin',
|
||||
'last_txid': name_rec['txid'],
|
||||
'did': name_rec.get('did', {'error': 'Not supported for this name'})
|
||||
}
|
||||
|
||||
else:
|
||||
@@ -623,7 +640,8 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
'expire_block': name_rec['expire_block'], # expire_block is what blockstack.js expects
|
||||
'renewal_deadline': name_rec['renewal_deadline'],
|
||||
'grace_period': name_rec.get('grace_period', False),
|
||||
'resolver': name_rec.get('resolver', None)
|
||||
'resolver': name_rec.get('resolver', None),
|
||||
'did': name_rec.get('did', {'error': 'Not supported for this name'})
|
||||
}
|
||||
|
||||
return self._reply_json(ret)
|
||||
@@ -859,6 +877,29 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
return
|
||||
|
||||
|
||||
def GET_did(self, path_info, did):
|
||||
"""
|
||||
Get a user profile.
|
||||
Reply the profile on success
|
||||
Return 404 on failure to load
|
||||
"""
|
||||
try:
|
||||
did_info = parse_DID(did)
|
||||
assert did_info['name_type'] in ('name', 'subdomain')
|
||||
except Exception as e:
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(e)
|
||||
|
||||
return self._reply_json({'error': 'Invalid DID'}, status_code=400)
|
||||
|
||||
blockstackd_url = get_blockstackd_url()
|
||||
resp = blockstackd_client.resolve_DID(did, hostport=blockstackd_url)
|
||||
if json_is_error(resp):
|
||||
return self._reply_json({'error': resp['error']}, status_code=404)
|
||||
|
||||
return self._reply_json({'public_key': resp['public_key'], 'document': resp['document']})
|
||||
|
||||
|
||||
def GET_user_profile( self, path_info, user_id ):
|
||||
"""
|
||||
@@ -1324,6 +1365,11 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
'GET': self.GET_blockchain_consensus,
|
||||
},
|
||||
},
|
||||
r'^/v1/dids/(did:stack:v0:{}+-[0-9]+)$'.format(OP_BASE58CHECK_CLASS): {
|
||||
'routes': {
|
||||
'GET': self.GET_did,
|
||||
},
|
||||
},
|
||||
r'^/v1/names$': {
|
||||
'routes': {
|
||||
'GET': self.GET_names,
|
||||
@@ -1399,6 +1445,11 @@ class BlockstackAPIEndpointHandler(SimpleHTTPRequestHandler):
|
||||
'GET': self.GET_subdomains
|
||||
},
|
||||
},
|
||||
r'^/v1/subdomains/([0-9a-fA-F]{64})$': {
|
||||
'routes': {
|
||||
'GET': self.GET_subdomain_ops,
|
||||
},
|
||||
},
|
||||
r'^/v1/users/({}{{1,256}})$'.format(URLENCODING_CLASS): {
|
||||
'routes': {
|
||||
'GET': self.GET_user_profile,
|
||||
|
||||
@@ -155,6 +155,11 @@ USER_ZONEFILE_SCHEMA = {
|
||||
OP_HISTORY_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'accepted': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
'maximum': 1,
|
||||
},
|
||||
'address': {
|
||||
'type': 'string',
|
||||
'pattern': OP_ADDRESS_PATTERN,
|
||||
@@ -187,6 +192,10 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'block_height': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'coeff': {
|
||||
'anyOf': [
|
||||
{
|
||||
@@ -225,6 +234,10 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'fully_qualified_subdomain': {
|
||||
'type': 'string',
|
||||
'pattern': OP_SUBDOMAIN_NAME_PATTERN,
|
||||
},
|
||||
'history_snapshot': {
|
||||
'type': 'boolean',
|
||||
},
|
||||
@@ -254,6 +267,16 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'missing': {
|
||||
'anyOf': [
|
||||
{
|
||||
'type': 'string',
|
||||
},
|
||||
{
|
||||
'type': 'null',
|
||||
},
|
||||
],
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'pattern': OP_NAME_OR_SUBDOMAIN_PATTERN,
|
||||
@@ -273,6 +296,18 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'string',
|
||||
'pattern': OP_CODE_NAME_PATTERN,
|
||||
},
|
||||
'owner': {
|
||||
'type': 'string',
|
||||
'pattern': OP_ADDRESS_PATTERN,
|
||||
},
|
||||
'parent_zonefile_hash': {
|
||||
'type': 'string',
|
||||
'pattern': OP_ZONEFILE_HASH_PATTERN,
|
||||
},
|
||||
'parent_zonefile_index': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'pending': {
|
||||
'type': 'boolean'
|
||||
},
|
||||
@@ -298,6 +333,10 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
'signature': {
|
||||
'type': 'string',
|
||||
'pattern': OP_BASE64_EMPTY_PATTERN,
|
||||
},
|
||||
'recipient': {
|
||||
'anyOf': [
|
||||
{
|
||||
@@ -372,6 +411,14 @@ OP_HISTORY_SCHEMA = {
|
||||
'type': 'string',
|
||||
'pattern': OP_BASE64_EMPTY_PATTERN,
|
||||
},
|
||||
'zonefile_hash': {
|
||||
'type': 'string',
|
||||
'pattern': OP_ZONEFILE_HASH_PATTERN,
|
||||
},
|
||||
'zonefile_offset': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
},
|
||||
'required': [
|
||||
'txid',
|
||||
@@ -585,6 +632,22 @@ SUBDOMAIN_SCHEMA_REQUIRED = [
|
||||
'value_hash',
|
||||
]
|
||||
|
||||
SUBDOMAIN_HISTORY_REQUIRED = [
|
||||
'fully_qualified_subdomain',
|
||||
'domain',
|
||||
'sequence',
|
||||
'owner',
|
||||
'zonefile_hash',
|
||||
'signature',
|
||||
'block_height',
|
||||
'parent_zonefile_hash',
|
||||
'parent_zonefile_index',
|
||||
'zonefile_offset',
|
||||
'txid',
|
||||
'missing',
|
||||
'resolver',
|
||||
]
|
||||
|
||||
ACCOUNT_SCHEMA_REQUIRED = [
|
||||
'address',
|
||||
'type',
|
||||
|
||||
@@ -1198,6 +1198,31 @@ class SubdomainDB(object):
|
||||
return self._extract_subdomain(rowdata)
|
||||
|
||||
|
||||
def get_subdomain_ops_at_txid(self, txid, cur=None):
|
||||
"""
|
||||
Given a txid, get all subdomain operations at that txid.
|
||||
Include unaccepted operations.
|
||||
Order by zone file index
|
||||
"""
|
||||
get_cmd = 'SELECT * FROM {} WHERE txid = ? ORDER BY zonefile_offset'.format(self.subdomain_table)
|
||||
|
||||
cursor = None
|
||||
if cur is None:
|
||||
cursor = self.conn.cursor()
|
||||
else:
|
||||
cursor = cur
|
||||
|
||||
db_query_execute(cursor, get_cmd, (txid,))
|
||||
|
||||
try:
|
||||
return [x for x in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
if BLOCKSTACK_DEBUG:
|
||||
log.exception(e)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_subdomains_owned_by_address(self, owner, cur=None):
|
||||
"""
|
||||
Get the list of subdomain names that are owned by a given address.
|
||||
@@ -2055,6 +2080,25 @@ def get_all_subdomains(offset=None, count=None, min_sequence=None, db_path=None,
|
||||
return db.get_all_subdomains(offset=offset, count=count, min_sequence=None)
|
||||
|
||||
|
||||
def get_subdomain_ops_at_txid(txid, db_path=None, zonefiles_dir=None):
|
||||
"""
|
||||
Static method for getting the list of subdomain operations accepted at a given txid.
|
||||
Includes unaccepted subdomain operations
|
||||
"""
|
||||
opts = get_blockstack_opts()
|
||||
if not is_subdomains_enabled(opts):
|
||||
return []
|
||||
|
||||
if db_path is None:
|
||||
db_path = opts['subdomaindb_path']
|
||||
|
||||
if zonefiles_dir is None:
|
||||
zonefiles_dir = opts['zonefiles']
|
||||
|
||||
db = SubdomainDB(db_path, zonefiles_dir)
|
||||
return db.get_subdomain_ops_at_txid(txid)
|
||||
|
||||
|
||||
def get_subdomains_owned_by_address(address, db_path=None, zonefiles_dir=None):
|
||||
"""
|
||||
Static method for getting the list of subdomains for a given address
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# this is the only place where version should be updated
|
||||
__version_major__ = '20'
|
||||
__version_minor__ = '0'
|
||||
__version_patch__ = '0'
|
||||
__version_patch__ = '2'
|
||||
__version__ = '{}.{}.{}.0'.format(__version_major__, __version_minor__, __version_patch__)
|
||||
|
||||
@@ -559,6 +559,99 @@ Get a history of all blockchain records of a registered name.
|
||||
},
|
||||
}
|
||||
|
||||
## Get subdomains at transaction [GET /v1/subdomains/{txid}]
|
||||
Fetches the list of subdomain operations processed by a given transaction.
|
||||
The returned array includes subdomain operations that have not yet been accepted
|
||||
as part of any subdomain's history (checkable via the `accepted` field). If the
|
||||
given transaction ID does not correspond to a Blockstack transaction that
|
||||
introduced new subdomain operations, and empty array will be returned.
|
||||
|
||||
+ Public Endpoint
|
||||
+ Subdomain aware
|
||||
+ Parameters
|
||||
+ txid: d04d708472ea3c147f50e43264efdb1535f71974053126dc4db67b3ac19d41fe (string) the transaction ID
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
[
|
||||
{
|
||||
"accepted": 1,
|
||||
"block_height": 546199,
|
||||
"domain": "id.blockstack",
|
||||
"fully_qualified_subdomain": "nturl345.id.blockstack",
|
||||
"missing": "",
|
||||
"owner": "17Q8hcsxRLCk3ypJiGeXQv9tFK9GnHr5Ea",
|
||||
"parent_zonefile_hash": "58224144791919f6206251a9960a2dd5723b96b6",
|
||||
"parent_zonefile_index": 95780,
|
||||
"resolver": "https://registrar.blockstack.org",
|
||||
"sequence": 0,
|
||||
"signature": "None",
|
||||
"txid": "d04d708472ea3c147f50e43264efdb1535f71974053126dc4db67b3ac19d41fe",
|
||||
"zonefile_hash": "d3bdf1cf010aac3f21fac473e41450f5357e0817",
|
||||
"zonefile_offset": 0
|
||||
},
|
||||
{
|
||||
"accepted": 1,
|
||||
"block_height": 546199,
|
||||
"domain": "id.blockstack",
|
||||
"fully_qualified_subdomain": "dwerner1.id.blockstack",
|
||||
"missing": "",
|
||||
"owner": "17tFeKEBMUAAiHVsCgqKo8ccwYqq7aCn9X",
|
||||
"parent_zonefile_hash": "58224144791919f6206251a9960a2dd5723b96b6",
|
||||
"parent_zonefile_index": 95780,
|
||||
"resolver": "https://registrar.blockstack.org",
|
||||
"sequence": 0,
|
||||
"signature": "None",
|
||||
"txid": "d04d708472ea3c147f50e43264efdb1535f71974053126dc4db67b3ac19d41fe",
|
||||
"zonefile_hash": "ab79b1774fa7a4c5709b6ad4e5892fb7c0f79765",
|
||||
"zonefile_offset": 1
|
||||
}
|
||||
]
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'accepted': { 'type': 'integer', 'minimum': 0, 'maximum': 1 },
|
||||
'block_height': { 'type': 'integer', 'minimum': 0 },
|
||||
'domain': { 'type': 'string', 'pattern': '^([a-z0-9\\-_.+]{3,37})$|^([a-z0-9\\-_.+]){3,37}$' },
|
||||
'fully_qualified_subdomain: { 'type': 'string', 'pattern': '^([a-z0-9\\-_.+]{3,37})\.([a-z0-9\\-_.+]{3,37})$' },
|
||||
'missing': { 'type': 'string' },
|
||||
'owner': { 'type': 'string', 'pattern': "^([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)$" },
|
||||
'parent_zonefile_hash': { 'type': 'string', 'pattern': '^[0-9a-fA-F]{40}' },
|
||||
'parent_zonefile_index': { 'type': 'integer', 'minimum': 0 },
|
||||
'resolver': { 'type': 'string' },
|
||||
'sequence': { 'type': 'integer', 'minimum': 0 },
|
||||
'signature': { 'type': 'string' },
|
||||
'txid': { 'type': 'string', 'pattern': '^[0-9a-fA-F]{64}' },
|
||||
'zonefile_hash': { 'type': 'string', 'pattern': '^[0-9a-fA-F]{40}' },
|
||||
'zonefile_offset': { 'type': 'integer', 'minimum': 0 }
|
||||
},
|
||||
'required': [ 'accepted, 'block_height, 'domain',
|
||||
'fully_qualified_subdomain', 'missing', 'owner',
|
||||
'parent_zonefile_hash', 'parent_zonefile_index', 'resolver',
|
||||
'sequence', 'signature', 'txid', 'zonefile_hash',
|
||||
'zonefile_offset' ]
|
||||
}
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{ "error": "Invalid txid" }
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'error': { 'type': 'string' },
|
||||
},
|
||||
}
|
||||
|
||||
## Get historical zone file [GET /v1/names/{name}/zonefile/{zoneFileHash}]
|
||||
Fetches the historical zonefile specified by the username and zone hash.
|
||||
+ Public Endpoint
|
||||
@@ -666,113 +759,6 @@ Retrieves a list of names owned by the address provided.
|
||||
|
||||
# Group Price Checks
|
||||
|
||||
## Get namespace price [GET /v1/prices/namespaces/{tld}]
|
||||
|
||||
This endpoint is used to get the price of a namespace.
|
||||
|
||||
+ Public Endpoint
|
||||
+ Parameters
|
||||
+ tld: id (string) - namespace to query price for
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"satoshis": 4000000000,
|
||||
"units": "BTC",
|
||||
"amount": "4000000000"
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'units': {
|
||||
'type': 'string',
|
||||
},
|
||||
'amount': {
|
||||
'type': 'string',
|
||||
'pattern': '^[0-9]+$',
|
||||
},
|
||||
'satoshis': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
},
|
||||
'required': [ 'satoshis' ]
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{ "error": "Invalid namepace" }
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'error': { 'type': 'string' },
|
||||
},
|
||||
}
|
||||
|
||||
## Get name price [GET /v1/prices/names/{name}]
|
||||
|
||||
This endpoint is used to get the price of a name. If you are using a public
|
||||
endpoint, you should *only* rely on the `name_price` field in the returned JSON
|
||||
blob.
|
||||
|
||||
The other fields are relevant only for estimating the cost of registering a
|
||||
name. You register a name via
|
||||
[blockstack.js](https://github.com/blockstack/blockstack.js) or the [Blockstack
|
||||
Browser](https://github.com/blockstack/blockstack-browser)).
|
||||
|
||||
+ Public Endpoint
|
||||
+ Parameters
|
||||
+ name: muneeb.id (string) - name to query price information for
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"name_price": {
|
||||
"satoshis": 100000,
|
||||
"units": "BTC",
|
||||
"amount": "100000"
|
||||
},
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name_price': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'satoshis': { 'type': 'integer', 'minimum': 0 },
|
||||
'units': { 'type': 'string' },
|
||||
'amount': { 'type': 'string', 'pattern': '^[0-9]+$' }
|
||||
},
|
||||
'required': [ 'satoshis' ],
|
||||
},
|
||||
'required': [ 'name_price' ]
|
||||
}
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{ "error": "Invalid name" }
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'error': { 'type': 'string' },
|
||||
},
|
||||
}
|
||||
|
||||
## Get namespace price [GET /v2/prices/namespaces/{tld}]
|
||||
|
||||
This endpoint is used to get the price of a namespace, while explicitly
|
||||
@@ -869,6 +855,107 @@ cryptocurrency (not necessarily Bitcoin).
|
||||
},
|
||||
},
|
||||
|
||||
## Legacy Get namespace price [GET /v1/prices/namespaces/{tld}]
|
||||
|
||||
This endpoint is used to get the price of a namespace in Bitcoin.
|
||||
|
||||
+ Public Endpoint
|
||||
+ Legacy Endpoint
|
||||
+ Parameters
|
||||
+ tld: id (string) - namespace to query price for
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"satoshis": 4000000000,
|
||||
"units": "BTC",
|
||||
"amount": "4000000000"
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'units': {
|
||||
'type': 'string',
|
||||
},
|
||||
'amount': {
|
||||
'type': 'string',
|
||||
'pattern': '^[0-9]+$',
|
||||
},
|
||||
'satoshis': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
},
|
||||
},
|
||||
'required': [ 'satoshis' ]
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{ "error": "Invalid namepace" }
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'error': { 'type': 'string' },
|
||||
},
|
||||
}
|
||||
|
||||
## Legacy Get name price [GET /v1/prices/names/{name}]
|
||||
|
||||
This endpoint is used to get the price of a name in Bitcoin.
|
||||
|
||||
+ Public Endpoint
|
||||
+ Legacy Endpoint
|
||||
+ Parameters
|
||||
+ name: muneeb.id (string) - name to query price information for
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"name_price": {
|
||||
"satoshis": 100000,
|
||||
"units": "BTC",
|
||||
"amount": "100000"
|
||||
},
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name_price': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'satoshis': { 'type': 'integer', 'minimum': 0 },
|
||||
'units': { 'type': 'string' },
|
||||
'amount': { 'type': 'string', 'pattern': '^[0-9]+$' }
|
||||
},
|
||||
'required': [ 'satoshis' ],
|
||||
},
|
||||
'required': [ 'name_price' ]
|
||||
}
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{ "error": "Invalid name" }
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'error': { 'type': 'string' },
|
||||
},
|
||||
}
|
||||
# Group Blockchain Operations
|
||||
|
||||
## Get consensus hash [GET /v1/blockchains/{blockchainName}/consensus]
|
||||
@@ -1324,8 +1411,8 @@ Get the Blockstack operations in a given block
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'address': {
|
||||
@@ -1514,9 +1601,9 @@ Get the Blockstack operations in a given block
|
||||
'txid',
|
||||
'vtxindex'
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
@@ -1553,23 +1640,28 @@ Get the Blockstack operations in a given block
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"namespaces": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
{
|
||||
"namespaces": [
|
||||
"id",
|
||||
"helloworld",
|
||||
"podcast",
|
||||
"graphite",
|
||||
"blockstack"
|
||||
]
|
||||
}
|
||||
|
||||
+ Schema
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'namespaces': {
|
||||
'type': 'array',
|
||||
'items': { 'type': 'string' }
|
||||
}
|
||||
},
|
||||
'required': [ 'namespaces' ]
|
||||
}
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'namespaces': {
|
||||
'type': 'array',
|
||||
'items': { 'type': 'string' }
|
||||
}
|
||||
},
|
||||
'required': [ 'namespaces' ]
|
||||
}
|
||||
|
||||
## Get namespace names [GET /v1/namespaces/{tld}/names?page={page}]
|
||||
|
||||
@@ -1627,7 +1719,7 @@ Fetch a list of names from the namespace.
|
||||
# Group Resolver Endpoints
|
||||
|
||||
## Lookup User [GET /v1/users/{username}]
|
||||
Lookup and resolver a user's profile. Defaults to the `id` namespace.
|
||||
Lookup and resolve a user's profile. Defaults to the `id` namespace.
|
||||
Note that [blockstack.js](https://github.com/blockstack/blockstack.js) does
|
||||
*not* rely on this endpoint.
|
||||
|
||||
@@ -1689,6 +1781,7 @@ Note that [blockstack.js](https://github.com/blockstack/blockstack.js) does
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'patternProperties': {
|
||||
@@ -1790,6 +1883,7 @@ Searches for a profile using a search string.
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -1804,3 +1898,93 @@ Searches for a profile using a search string.
|
||||
}
|
||||
}
|
||||
|
||||
## Resolve DID [GET /v1/dids/{did}]
|
||||
Resolve a Blockstack DID to its DID document object (DDO). In practice, the DDO
|
||||
is stored in the same way as a user profile, but a few extra DDO-specific
|
||||
fields will be filled in by this endpoint (namely, `@context` and `publicKey`).
|
||||
|
||||
Blockstack DIDs correspond to non-revoked, non-expired names. A DID will not
|
||||
resolve if its underlying name is revoked or expired, or if the DID does not
|
||||
correspond to an existing name.
|
||||
|
||||
+ Public Endpoint
|
||||
+ Subdomain Aware
|
||||
+ Parameters
|
||||
+ did: `did:stack:v0:15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr-0` (string) - DID to resolve
|
||||
+ Response 200 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"document": {
|
||||
"@context": "https://w3id.org/did/v1",
|
||||
"publicKey": [
|
||||
{
|
||||
"id": "did:stack:v0:15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr-0",
|
||||
"publicKeyHex": "022af593b4449b37899b34244448726aa30e9de13c518f6184a29df40823d82840",
|
||||
"type": "secp256k1"
|
||||
}
|
||||
],
|
||||
... omitted for brevity ...
|
||||
},
|
||||
"public_key": "022af593b4449b37899b34244448726aa30e9de13c518f6184a29df40823d82840"
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"document": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@context": { "type": "string" },
|
||||
"publicKey": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"type": { "type": "string" },
|
||||
"publicKeyHex": { "type": "string", "pattern": "^[0-9a-fA-F]$" },
|
||||
},
|
||||
"required": [ "id", "type", "publicKeyHex" ],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": [ "@context", "publicKey" ],
|
||||
},
|
||||
"public_key": { "type": "string", "pattern": "^[0-9a-fA-F]$" },
|
||||
}
|
||||
"required": [ "document", "public_key" ]
|
||||
}
|
||||
|
||||
|
||||
+ Response 400 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"error": "Invalid DID"
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': { 'error': 'string' },
|
||||
'required': [ 'error' ]
|
||||
}
|
||||
|
||||
+ Response 404 (application/json)
|
||||
+ Body
|
||||
|
||||
{
|
||||
"error": "Failed to get DID record: Failed to resolve DID to a non-revoked name"
|
||||
}
|
||||
|
||||
+ Schema
|
||||
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': { 'error': 'string' },
|
||||
'required': [ 'error' ]
|
||||
}
|
||||
|
||||
818
docs/blockstack-did-spec.md
Normal file
818
docs/blockstack-did-spec.md
Normal file
@@ -0,0 +1,818 @@
|
||||
# Blockstack DID Method Specification
|
||||
|
||||
# Abstract
|
||||
|
||||
Blockstack is a network for decentralized applications where users own their
|
||||
identities and data. Blockstack utilizes a public blockchain to implement a
|
||||
decentralized [naming layer](https://docs.blockstack.org/core/naming/introduction.html),
|
||||
which binds a user's human-readable username to their current public key and a pointer to
|
||||
their data storage buckets. The naming layer ensures that names are globally
|
||||
unique, that names can be arbitrary human-meaningful strings, and that names
|
||||
are owned and controlled by cryptographic key pairs such that only the owner of the private key
|
||||
can update the name's associated state.
|
||||
|
||||
The naming layer implements DIDs as a mapping
|
||||
between the initial name operation for a user's name and the name's current
|
||||
public key. The storage pointers in the naming layer are leveraged to point to
|
||||
the authoritative replica of the user's DID document.
|
||||
|
||||
# Status of This Document
|
||||
|
||||
This document is not a W3C Standard nor is it on the W3C Standards Track. This is
|
||||
a draft document and may be updated, replaced or obsoleted by other documents at
|
||||
any time. It is inappropriate to cite this document as other than work in
|
||||
progress.
|
||||
|
||||
Comments regarding this document are welcome. Please file issues directly on
|
||||
[Github](https://github.com/blockstack/blockstack-core/blob/master/docs/did-spec.md).
|
||||
|
||||
# 1. System Overview
|
||||
|
||||
Blockstack's DID method is specified as part of its decentralized naming system.
|
||||
Each name in Blockstack has one or more corresponding DIDs, and each Blockstack
|
||||
DID corresponds to exactly one name -- even if the name was revoked by its
|
||||
owner, expired, or was re-registered to a different owner.
|
||||
|
||||
Blockstack is unique among decentralized identity systems in that it is *not*
|
||||
anchored to a specific blockchain or DLT implementation. The system is designed
|
||||
from the ground up to be portable, and has already been live-migrated from the
|
||||
Namecoin blockchain to the Bitcoin blockchain. The operational ethos of
|
||||
Blockstack is to leverage the must secure blockchain at all times -- that is,
|
||||
the one that is considered hardest to attack.
|
||||
|
||||
Blockstack's naming system and its DIDs transcend the underlying blockchain, and
|
||||
will continue to resolve to DID document objects (DDOs) even if the system
|
||||
migrates to a new blockchain in the future.
|
||||
|
||||
## 1.1 DID Lifecycle
|
||||
|
||||
Understanding how Blockstack DIDs operate requires understanding how Blockstack
|
||||
names operate. Fundamentally, a Blockstack DID is defined as a pointer to the
|
||||
*nth name registered by an address.* How this information is determined depends
|
||||
on the category of name being registered -- a DID can be derived from an
|
||||
*on-chain* name or an *off-chain* name. We call these DIDs *on-chain DIDs* and
|
||||
*off-chain DIDs*, respectively.
|
||||
|
||||
### 1.1.1 On-Chain DIDs
|
||||
|
||||
On-chain names are written directly to Blockstack's underlying blockchain.
|
||||
Instantiating an on-chain name requires two transactions -- a `NAME_PREORDER`
|
||||
transaction, and a `NAME_REGISTRATION` transaction. Upon successful
|
||||
confirmation of the `NAME_REGISTRATION` transaction, the system assigns name to
|
||||
an on-chain address indicated by the `NAME_REGISTRATION` transaction itself.
|
||||
This address is the name's *owner*.
|
||||
|
||||
Since these transactions are written to the blockchain, the blockchain provides
|
||||
a total ordering of name-to-address assignments. Thus, a DID instanted for an
|
||||
on-chain name may be referenced by the name's owner address, and the number of
|
||||
names evern assigned to the owner address *at the time of this DID's
|
||||
instantiation*. For example, the DID
|
||||
`did:stack:v0:15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr-3` was instantiated when the
|
||||
fourth on-chain name was created and initially assigned to the address `15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr`
|
||||
(note that the index parameter -- the `-3` -- starts counting from 0).
|
||||
|
||||
### 1.1.2 Off-chain DIDs
|
||||
|
||||
Off-chain names, sometimes called *subdomains* in the Blockstack literature,
|
||||
refer to names whose transaction histories are instantiated and stored outside
|
||||
of Blockstack's blockchain within Blockstack's Atlas peer network. Off-chain
|
||||
name transactions are encoded in batches, where each batch is hashed and written
|
||||
to the underlying blockchain through a transaction for an on-chain name. This
|
||||
provides them with the same safety properties as on-chain names -- off-chain
|
||||
names are globally unique, off-chain names can be arbitrary human-meaningful
|
||||
strings, off-chain names are owned by cryptographic key pairs, and all
|
||||
Blockstack nodes see the same linearized history of off-chain name operations.
|
||||
|
||||
Off-chain names are instantiated by an on-chain name, indicated by the off-chain
|
||||
name's suffix. For example, `cicero.res_publica.id` is an off-chain name
|
||||
whose initial transaction history is processed by the owner of the on-chain
|
||||
name `res_publica.id`. Note that the owner of `res_publica.id` does *not* own
|
||||
`cicero.res_publica.id`, and cannot issue well-formed name updates to it.
|
||||
|
||||
Off-chain names -- and by extension, their corresponding DIDs -- have
|
||||
different liveness properties than on-chain names. The Blockstack
|
||||
naming system protocol requires the owner of `res_publica.id` to not only
|
||||
propagate the signed transactions that instantiate and transfer ownership of
|
||||
`cicero.res_publica.id`. However, *any* on-chain name can process a name update
|
||||
for an off-chain name -- that is, an update that changes where the name's
|
||||
assocaited state resides. For details as to why this is the case, please refer
|
||||
to the [Blockstack subdomain documentation](https://docs.blockstack.org/core/naming/subdomains.html).
|
||||
|
||||
An off-chain DID is similarly structured to an on-chain DID. Like on-chain
|
||||
names, each off-chain name is owned by an address (but not necessarily an
|
||||
address on the blockchain), and each Blockstack node sees the same sequence of
|
||||
off-chain name-to-address assignments. Thus, it has enough information to
|
||||
assign each off-chain name user a DID.
|
||||
|
||||
# 2. Blockstack DID Method
|
||||
|
||||
The namestring that shall identify this DID method is: `stack`
|
||||
|
||||
A DID that uses this method *MUST* begin with the following literal prefix: `did:stack`.
|
||||
The remainder of the DID is its namespace-specific identifier.
|
||||
|
||||
# 2.1 Namespace-Specific Identifier
|
||||
|
||||
The namespace-specific identifier (NSI) of the Blockstack DID encodes two pieces
|
||||
of information: an address, and an index.
|
||||
|
||||
The **address** shall be a base58check encoding of a version byte concatenated with
|
||||
the RIPEMD160 hash of a SHA256 hash of a DER-encoded secp256k1 public key.
|
||||
For example, in this Python 2 snippit:
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import base58
|
||||
|
||||
pubkey = '042bc8aa4eb54d779c1fb8a2d5022aec8ed7fc2cc34d57356d9e1c417ce416773f45b0299ea7be347d14c69c403d9a03c8ec0ccf47533b4bee8cd002e5de81f945'
|
||||
sha256_pubkey = hashlib.sha256(pubkey.decode('hex')).hexdigest()
|
||||
# '18328b13b4df87cbcd190c083ef1d74487fc1383792f208f52c596b4588fb665'
|
||||
ripemd160_sha256_pubkey = hashlib.new('ripemd160', sha256_pubkey.decode('hex')).hexdigest()
|
||||
# '1651c1a6001d4750e46be8a02cc19550d4309b71'
|
||||
version_byte = '\x00'
|
||||
address = base58.b58check_encode(version_byte + ripemd160_sha256_pubkey.decode('hex'))
|
||||
# '1331okvQ3Jr2efzaJE42Supevzfzg8ahYW'
|
||||
```
|
||||
|
||||
The **index** shall be a non-negative monotonically-increasing integer.
|
||||
|
||||
The (address, index) pair uniquely identifies a DID. Blockstack's naming system
|
||||
ensures that the index increments monotonically each time a DID is instantiated
|
||||
(e.g. by incrementing it each time a name gets registered to the address).
|
||||
|
||||
## 2.2 Address Encodings
|
||||
|
||||
The address's version byte encodes whether or not a DID corresponds to an
|
||||
on-chain name transaction or an off-chain name transaction, and whether or not
|
||||
it corresponds to a mainnet or testnet address. The version bytes for each
|
||||
configuration shall be as follows:
|
||||
|
||||
* On-chain names on mainnet: `0x00`
|
||||
* On-chain names on testnet: `0x6f`
|
||||
* Off-chain names on mainnet: `0x3f`
|
||||
* Off-chain names on testnet: `0x7f`
|
||||
|
||||
For example, the RIPEMD160 hash `1651c1a6001d4750e46be8a02cc19550d4309b71` would
|
||||
encode to the following base58check strings:
|
||||
|
||||
* On-chain mainnet: `1331okvQ3Jr2efzaJE42Supevzfzg8ahYW`
|
||||
* On-chain testnet: `mhYy6p1NrLHHRnUC1o2QGq2ynzGhduVoEX`
|
||||
* Off-chain mainnet: `SPL1qbhYmg3EAyn2qf36zoyDamuRXm2Gjk`
|
||||
* Off-chain testnet: `t8xcrYmzDDhJWihaQWMW2qPZs4Po1PfvCB`
|
||||
|
||||
# 3. Blockstack DID Operations
|
||||
|
||||
## 3.1 Creating a Blockstack DID
|
||||
|
||||
Creating a Blockstack DID requires registering a name -- be it on-chain or
|
||||
off-chain. To register an on-chain name, the user must send two transactions to
|
||||
Blockstack's underlying blockchain (currently Bitcoin) that implement the
|
||||
`NAME_PREORDER` and `NAME_REGISTRATION` commands. Details on the wire formats
|
||||
for these transactions can be found in Appendix A. Blockstack supplies both a
|
||||
[graphical tool](https://github.com/blockstack/blockstack-browser) and a
|
||||
[command-line tool](https://github.com/blockstackl/cli-blockstack) for
|
||||
generating and broadcasting these transactions, as well as a
|
||||
[reference library](https://github.com/blockstack/blockstack.js).
|
||||
|
||||
To register an off-chain name, the user must be able to submit a request to an
|
||||
off-chain registrar. Anyone with an on-chain name can use it to operate a
|
||||
registrar for off-chain names. A reference implementation can be found
|
||||
[here](https://github.com/blockstack/subdomain-registrar).
|
||||
|
||||
To register an off-chain DID, the user
|
||||
must submit a JSON body as a HTTP POST request to the registrar's
|
||||
registration endpoint with the following format:
|
||||
|
||||
```
|
||||
{
|
||||
"zonefile": "<zonefile encoding the location of the DDO>",
|
||||
"name": "<off-chain name, excluding the on-chain suffix>",
|
||||
"owner_address": "<b58check-encoded address that will own the name, with version byte \x00>",
|
||||
}
|
||||
```
|
||||
|
||||
For example, to register the name `spqr` on a registrar for `res_publica.id`:
|
||||
|
||||
```bash
|
||||
$ curl -X POST -H 'Authorization: bearer API-KEY-IF-USED' -H 'Content-Type: application/json' \
|
||||
> --data '{"zonefile": "$ORIGIN spqr\n$TTL 3600\n_https._tcp URI 10 1 \"https://gaia.blockstack.org/hub/1HgW81v6MxGD76UwNbHXBi6Zre2fK8TwNi/profile.json\"\n", \
|
||||
> "name": "spqr", \
|
||||
> "owner_address": "1HgW81v6MxGD76UwNbHXBi6Zre2fK8TwNi"}' \
|
||||
> http://localhost:3000/register/
|
||||
```
|
||||
|
||||
The `zonefile` field must be a well-formed DNS zonefile, and must have the
|
||||
following properties:
|
||||
|
||||
* It must have its `$ORIGIN` field set to the off-chain name.
|
||||
* It must have at least one `URI` resource record that encodes an HTTP or
|
||||
HTTPS URL. Note that its name must be either `_http._tcp` or `_https._tcp`, per the
|
||||
`URI` record specification.
|
||||
* The HTTP or HTTPS URL must resolve to a JSON Web token signed by a secp256k1 public key
|
||||
that hashes to the `owner_address` field, per section 2.1.
|
||||
|
||||
Once the request is accepted, the registrar will issue a subsequent `NAME_UPDATE`
|
||||
transaction for its on-chain name and broadcast the batch of off-chain zone
|
||||
files it has accumulated to the Blockstack Atlas network (see Appendix A). The batch
|
||||
of off-chain names' zone files will be hashed, and the hash will be written to
|
||||
the blockchain as part of the `NAME_UPDATE`. This proves the existence of these
|
||||
off-chain names, as well as their corresponding DIDs.
|
||||
|
||||
Once the transaction confirms and the off-chain zone files are propagated to the
|
||||
peer network, any Blockstack node will be able to resolve the off-chain name's associated DID.
|
||||
|
||||
## 3.2 Storing a Blockstack DID's DDO
|
||||
|
||||
Each name in Blockstack, and by extention, each DID, must have one or more
|
||||
associated URLs. To resolve a DID (section 3.3), the DID's URLs must point to
|
||||
a well-formed signed DDO. It is up to the DID owner to sign and upload the DDO
|
||||
to the relevant location(s) so that DID resolution works as expected, and it is
|
||||
up to the DID owner to ensure that the DDO is well-formed. Resolvers should
|
||||
validate DDOs before returning them to clients.
|
||||
|
||||
In order for a DID to resolve to a DDO, the DDO must be encoded as a JSON web
|
||||
token, and must be signed by the secp256k1 private key whose public key hashes
|
||||
to the DID's address. This is used by the DID resolver to authenticate the DDO,
|
||||
thereby removing the need to trust the server(s) hosting the DDO with replying
|
||||
authentic data.
|
||||
|
||||
## 3.3 Resolving a Blockstack DID
|
||||
|
||||
Any Blockstack node with an up-to-date view of the underlying blockchain and a
|
||||
complete set of off-chain zone files can translate any name into its DID, and
|
||||
translate any DID into its name.
|
||||
|
||||
Since DID registration in Blockstack is achieved by first registering a name,
|
||||
the user must first determine the DID's NSI. To do so, the user simply requests
|
||||
it from a Blockstack node of their choice as a GET request to the node's
|
||||
`/v1/dids/{:blockstack_did}` endpoint. The response must be a JSON object with
|
||||
a `public_key` field containing the secp256k1 public key that hashes to the
|
||||
DID's address, and a `document` field containing the DDO. The DDO's `publicKey` field
|
||||
shall be an array of objects with one element, where the
|
||||
only element describes the `public_key` given in the top-level object.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ curl -s https://core.blockstack.org/v1/dids/did:stack:v0:15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr-0 | jq
|
||||
{
|
||||
'public_key': '022af593b4449b37899b34244448726aa30e9de13c518f6184a29df40823d82840',
|
||||
'document': {
|
||||
...
|
||||
'@context': 'https://w3id.org/did/v1',
|
||||
'publicKey': [
|
||||
{
|
||||
'id': 'did:stack:v0:15gxXgJyT5tM5A4Cbx99nwccynHYsBouzr-0',
|
||||
'type': 'secp256k1',
|
||||
'publicKeyHex': '022af593b4449b37899b34244448726aa30e9de13c518f6184a29df40823d82840'
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 Updating a Blockstack DID
|
||||
|
||||
The user can change their DDO at any time by uploading a new signed DDO to the
|
||||
relevant locations, per section 3.2, *except for* the `publicKey` field. In
|
||||
order to change the DID's public key, the user must transfer the underlying name
|
||||
to a new address.
|
||||
|
||||
If the DID corresponds to an on-chain name, then the user must send a
|
||||
`NAME_TRANSFER` transaction to send the name to the new address. Once the
|
||||
transaction is confirmed by the Blockstack network, the DID's public key will be
|
||||
updated. See Appendix A for the `NAME_TRANSFER` wire format. Blockstack
|
||||
provides a [reference library](https://github.com/blockstack/blockstack.js) for
|
||||
generating this transaction.
|
||||
|
||||
### 3.4.1 Off-Chain DID Updates
|
||||
|
||||
If the DID corresponds to an off-chain name, then the user must request that the
|
||||
registrar that instantiated the name to broadcast an off-chain name transfer
|
||||
operation. To do so, the user must submit a string with the following format to
|
||||
the registrar:
|
||||
|
||||
```
|
||||
${name} TXT "owner=${new_address}" "seqn=${update_counter}" "parts=${length_of_zonefile_base64}" "zf0=${base64_part_0}" "zf1=${base64_part_1}" ... "sig=${base64_signature}"
|
||||
```
|
||||
|
||||
The string is a well-formed DNS TXT record with the following fields:
|
||||
|
||||
* The `${name}` field is the subdomain name without the on-chain suffix (e.g.
|
||||
`spqr` in `spqr.res_publica.id`.
|
||||
* The `${new_address}` field is the new owner address of the subdomain name.
|
||||
* The `${update_counter}` field is a non-negative integer equal to the number of
|
||||
subdomain name operations that have occurred so far. It starts with 0 when
|
||||
the name is created, and must increment each time the name owner issues an
|
||||
off-chain name operation.
|
||||
* The `${length_of_zonefile_base64}` field is equal to the length of the
|
||||
base64-encoded zone file for the off-chain name.
|
||||
* The fields `zf0`, `zf1`, `zf2`, etc. and their corresponding variables
|
||||
`${base64_part_0}`, `${base64_part_1}`, `${base64_part_2}`, etc. correspond to
|
||||
256-byte segments of the base64-encoded zone file. They must occur in a
|
||||
sequence of `zf${n}` where `${n}` starts at 0 and increments by 1 until all
|
||||
segments of the zone file are represented.
|
||||
* The `${base64_signature}` field is a secp256k1 signature over the resulting
|
||||
string, up to the `sig=` field, and base64-encoded. The signature must come
|
||||
from the secp256k1 private key that currently owns the name.
|
||||
|
||||
Thus to generate this TXT record for their DID, the user would do the following:
|
||||
|
||||
1. Base64-encode the off-chain DID's zone file.
|
||||
2. Break the base64-encoded zone file into 256-byte segments.
|
||||
3. Assemble the TXT record from the name, new address, update counter, and zone
|
||||
file segments.
|
||||
4. Sign the resulting string with the DID's current private key.
|
||||
5. Generate and append the `sig=${base64_signature}` field to the TXT record.
|
||||
|
||||
Sample code to generate these TXT records can be found in the [Blockstack Core
|
||||
reference implementation](https://github.com/blockstack/blockstack-core), under
|
||||
the `blockstack.lib.subdomains` package. For example, the Python 2 program here
|
||||
generates such a TXT record:
|
||||
|
||||
```python
|
||||
import blockstack
|
||||
|
||||
offchain_name = 'bar'
|
||||
onchain_name = 'foo.test'
|
||||
new_address = '1Jq3x8BAYz9Xy9AMfur5PXkDsWtmBBsNnC'
|
||||
seqn = 1
|
||||
privk = 'da1182302fee950e64241a4103646992b1bed7f6c4ced858282e493d57df33a501'
|
||||
full_name = '{}.{}'.format(offchain_name, onchain_name)
|
||||
zonefile = "$ORIGIN {}\n$TTL 3600\n_http._tcp\tIN\tURI\t10\t1\t\"https://gaia.blockstack.org/hub/{}/profile.json\"\n\n".format(offchain_name, new_address)
|
||||
|
||||
print blockstack.lib.subdomains.make_subdomain_txt(full_name, onchain_name, new_address, seqn, zonefile, privk)
|
||||
```
|
||||
|
||||
The program prints a string such as:
|
||||
```
|
||||
bar TXT "owner=1Jq3x8BAYz9Xy9AMfur5PXkDsWtmBBsNnC" "seqn=1" "parts=1" "zf0=JE9SSUdJTiBiYXIKJFRUTCAzNjAwCl9odHRwLl90Y3AJSU4JVVJJCTEwCTEJImh0dHBzOi8vZ2FpYS5ibG9ja3N0YWNrLm9yZy9odWIvMUpxM3g4QkFZejlYeTlBTWZ1cjVQWGtEc1d0bUJCc05uQy9wcm9maWxlLmpzb24iCgo\=" "sig=QEA+88Nh6pqkXI9x3UhjIepiWEOsnO+u1bOBgqy+YyjrYIEfbYc2Q8YUY2n8sIQUPEO2wRC39bHQHAw+amxzJfkhAxcC/fZ0kYIoRlh2xPLnYkLsa5k2fCtXqkJAtsAttt/V"
|
||||
```
|
||||
|
||||
(Note that the `sig=` field will differ between invocations, due to the way
|
||||
ECDSA signatures work).
|
||||
|
||||
Once this TXT record has been submitted to the name's original registrar, the
|
||||
registrar will pack it along with other such records into a single zone file,
|
||||
and issue a `NAME_UPDATE` transaction for the on-chain name to announce them to
|
||||
the rest of the peer network. The registrar will then propagate these TXT
|
||||
records to the peer network once the transaction confirms, thereby informing all
|
||||
Blockstack nodes of the new state of the off-chain DID.
|
||||
|
||||
### 3.4.2 Changing the Storage Locations of a DDO
|
||||
|
||||
If the user wants to change where the resolver will look for a DDO, they must do
|
||||
one of two things. If the DID corresponds to an on-chain name, then the user
|
||||
must send a `NAME_UPDATE` transaction for the underlying name, whose 20-byte
|
||||
hash field is the RIPEMD160 hash of the name's new zone file. See Appendix A
|
||||
for the wire format of `NAME_UPDATE` transactions.
|
||||
|
||||
If the DID corresponds to an off-chain name, then the user must submit a request
|
||||
to an off-chain name registrar to propagate a new zone file for the name.
|
||||
Unlike changing the public key, the user can ask *any* off-chain registrar to
|
||||
broadcast a new zone file. The method for doing this is described in section
|
||||
3.4.1 -- the user simply changes the zone file contents instead of the address.
|
||||
|
||||
# 4. Deleting a Blockstack DID
|
||||
|
||||
If the user wants to delete their DID, they can do so by revoking the underlying
|
||||
name. To do this with an on-chain name, the user constructs and broadcasts a
|
||||
`NAME_REVOKE` transaction. Once confirmed, the DID will stop resolving.
|
||||
|
||||
To do this with an off-chain name, the user constructs and broadcasts a TXT
|
||||
record for their DID's underlying name that (1) changes the owner address to a
|
||||
"nothing-up-my-sleeve" address (such as `1111111111111111111114oLvT2` -- the
|
||||
base58-check encoding of 20 bytes of 0's), and (2) changes the zone file to
|
||||
include an unresolvable URL. This prevents the DID from resolving, and prevents
|
||||
it from being updated.
|
||||
|
||||
# 5. Security Considerations
|
||||
|
||||
This section briefly outlines possible ways to attack Blockstack's DID method,
|
||||
as well as countermeasures the Blockstack protocol and the user can take to
|
||||
defend against them.
|
||||
|
||||
## 5.1 Public Blockchain Attacks
|
||||
|
||||
Blockstack operates on top of a public blockchain, which could be attacked by a
|
||||
sufficiently pwowerful adversary -- such as rolling back and changing the chain's
|
||||
transaction history, denying new transactions for Blockstack's name
|
||||
operations, or eclipsing nodes.
|
||||
|
||||
Blockstack makes the first two attacks difficult by operating on top of the most
|
||||
secure blockchain -- currently Bitcoin. If the blockchain is attacked, or a
|
||||
stronger blockchain comes into being, the Blockstack community would migrate the
|
||||
Blockstack network to a new blockchain.
|
||||
|
||||
The underlying blockchain provides some immunity towards eclipse attacks, since a
|
||||
blockchain peer expects blocks to arrive at roughly fixed intervals and expects
|
||||
blocks to have a proof of an expenditure of an expensive resource (like
|
||||
electricity). In Bitcoin's case, the computational difficulty of finding new blocks puts a
|
||||
high lower bound on the computational effort required to eclipse a Bitcoin node --
|
||||
in order to sustain 10-minute block times, the attacker must expend an equal
|
||||
amount of energy as the rest of the network. Moreover, the required expenditure
|
||||
rate (the "chain difficulty") decreases slowly enough that an attacker with less
|
||||
energy would have to spend months of time on the attack, giving the victim
|
||||
ample time to detect it. The countermeasures the blockchain employs to deter
|
||||
eclipse attacks are beyond the scope of this document, but it is worth pointing
|
||||
out that Blockstack's DID method benefits from them since they also help ensure
|
||||
that DID creation, updates and deletions get processed in a timely manner.
|
||||
|
||||
## 5.2 Blockstack Peer Network Attacks
|
||||
|
||||
Because Blockstack stores each DID's DDO's URL in its own peer network outside
|
||||
of its underlying blockchain, it is possible to eclipse Blockstack nodes and
|
||||
prevent them from seeing both off-chain DID operations and updates to on-chain
|
||||
DIDs. In an effort to make this as difficult as possible, the
|
||||
Blockstack peer network implements an unstructured overlay network -- nodes select
|
||||
a random sample of the peer graph as their neighbors. Moreover, Blockstack
|
||||
nodes strive to fetch a full replica of all zone files, and pull zone files from
|
||||
their neighbors in rarest-first order to prevent zone files from getting lost
|
||||
while they are propagating. This makes eclipsing a node
|
||||
maximally difficult -- an attacker would need to disrupt all of a the victim
|
||||
node's neighbor links.
|
||||
|
||||
In addition to this protocol-level countermeasure, a user has the option of
|
||||
uploading zone files manually to their preferred Blockstack nodes. If
|
||||
vigilent users have access to a replica of the zone files, they can re-seed
|
||||
Blockstack nodes that do not have them.
|
||||
|
||||
## 5.3 Stale Data and Replay Attacks
|
||||
|
||||
A DID's DDO is stored on a 3rd party storage provider. The DDO's public key is
|
||||
anchored to the blockchain, which means each time the DDO public key changes,
|
||||
all previous DDOs are invalidated. Similarly, the DDO's storage provider URLs
|
||||
are anchored to the blockchain, which means each time the DID's zone file
|
||||
changes, any stale DDOs will no longer be fetched. However, if the user changes
|
||||
other fields of their DDO, a malicious storage provider or a network adversary
|
||||
can serve a stale but otherwise valid DDO and the resolver will accept it.
|
||||
|
||||
The user has a choice of which storage providers host their DDO. If the storage
|
||||
provider serves stale data, the user can and should change their storage
|
||||
provider to one that will serve only fresh data. In addition, the user should
|
||||
use secure transport protocols like HTTPS to make replay attacks on the network
|
||||
difficult. For use cases where these are not sufficient to prevent replay
|
||||
attacks, the user should change their zone file and/or public key each time they
|
||||
change their DDO.
|
||||
|
||||
# 6. Privacy Considerations
|
||||
|
||||
Blockstack's DIDs are underpinned by Blockstack IDs (human readable
|
||||
names), and every Blockstack node records where every DID's DDO is
|
||||
hosted. However, users have the option of encrypting their DDOs so that only a
|
||||
select set of other users can decrypt them.
|
||||
|
||||
Blockstack's peer network and DID resolver use HTTP(S), meaning that
|
||||
intermediate middleboxes like CDNs and firewalls can cache data and log
|
||||
requests.
|
||||
|
||||
# 7. Reference Implementations
|
||||
|
||||
Blockstack implements a [RESTful API](https://core.blockstack.org) for querying
|
||||
DIDs. It also implements a [reference
|
||||
library](https://github.com/blockstack/blockstack.js) for generating well-formed
|
||||
on-chain transactions, and it implements a [Python
|
||||
library](https://github.com/blockstack/blockstack/core/blob/master/blockstack/lib/subdomains.py)
|
||||
for generating off-chain DID operations. The Blockstack node [reference
|
||||
implementation](https://github.com/blockstack/blockstack-core) is available
|
||||
under the terms of the General Public Licence, version 3.
|
||||
|
||||
# 8. Resources
|
||||
|
||||
Many Blockstack developers communicate via the [Blockstack
|
||||
Forum](https://forum.blockstack.org) and via the [Blockstack
|
||||
Slack](https://blockstack.slack.com). Interested developers are encouraged to
|
||||
join both.
|
||||
|
||||
# Appendix A: On-chain Wire Formats
|
||||
|
||||
This section is for organizations who want to be able to create and send name operation
|
||||
transactions to the blockchain(s) Blockstack supports.
|
||||
It describes the transaction formats for the Bitcoin blockchain.
|
||||
|
||||
Only the transactions that affect DID creation, updates, resolution, and
|
||||
deletions are documented here. A full listing of all Blockstack transaction
|
||||
formats can be found
|
||||
[here](https://github.com/blockstack/blockstack-core/blob/master/docs/wire-format.md).
|
||||
|
||||
## Transaction format
|
||||
|
||||
Each Bitcoin transaction for Blockstack contains signatures from two sets of keys: the name owner, and the payer. The owner `scriptSig` and `scriptPubKey` fields are generated from the key(s) that own the given name. The payer `scriptSig` and `scriptPubKey` fields are used to *subsidize* the operation. The owner keys do not pay for any operations; the owner keys only control the minimum amount of BTC required to make the transaction standard. The payer keys only pay for the transaction's fees, and (when required) they pay the name fee.
|
||||
|
||||
This construction is meant to allow the payer to be wholly separate from the owner. The principal that owns the name can fund their own transactions, or they can create a signed transaction that carries out the desired operation and request some other principal (e.g. a parent organization) to actually pay for and broadcast the transaction.
|
||||
|
||||
The general transaction layout is as follows:
|
||||
|
||||
| **Inputs** | **Outputs** |
|
||||
| ------------------------ | ----------------------- |
|
||||
| Owner scriptSig (1) | `OP_RETURN <payload>` (2) |
|
||||
| Payment scriptSig | Owner scriptPubKey (3) |
|
||||
| Payment scriptSig... (4) |
|
||||
| ... (4) | ... (5) |
|
||||
|
||||
(1) The owner `scriptSig` is *always* the first input.
|
||||
(2) The `OP_RETURN` script that describes the name operation is *always* the first output.
|
||||
(3) The owner `scriptPubKey` is *always* the second output.
|
||||
(4) The payer can use as many payment inputs as (s)he likes.
|
||||
(5) At most one output will be the "change" `scriptPubKey` for the payer.
|
||||
Different operations require different outputs.
|
||||
|
||||
## Payload Format
|
||||
|
||||
Each Blockstack transaction in Bitcoin describes the name operation within an `OP_RETURN` output. It encodes name ownership, name fees, and payments as `scriptPubKey` outputs. The specific operations are described below.
|
||||
|
||||
Each `OP_RETURN` payload *always* starts with the two-byte string `id` (called the "magic" bytes in this document), followed by a one-byte `op` that describes the operation.
|
||||
|
||||
### NAME_PREORDER
|
||||
|
||||
Op: `?`
|
||||
|
||||
Description: This transaction commits to the *hash* of a name. It is the first
|
||||
transaction of two transactions that must be sent to register a name in BNS.
|
||||
|
||||
Example: [6730ae09574d5935ffabe3dd63a9341ea54fafae62fde36c27738e9ee9c4e889](https://www.blocktrail.com/BTC/tx/6730ae09574d5935ffabe3dd63a9341ea54fafae62fde36c27738e9ee9c4e889)
|
||||
|
||||
`OP_RETURN` wire format:
|
||||
```
|
||||
0 2 3 23 39
|
||||
|-----|--|--------------------------------------------------|--------------|
|
||||
magic op hash_name(name.ns_id,script_pubkey,register_addr) consensus hash
|
||||
```
|
||||
|
||||
Inputs:
|
||||
* Payment `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
* `OP_RETURN` payload
|
||||
* Payment `scriptPubkey` script for change
|
||||
* `p2pkh` `scriptPubkey` to the burn address (0x00000000000000000000000000000000000000)
|
||||
|
||||
Notes:
|
||||
* `register_addr` is a base58check-encoded `ripemd160(sha256(pubkey))` (i.e. an address). This address **must not** have been used before in the underlying blockchain.
|
||||
* `script_pubkey` is either a `p2pkh` or `p2sh` compiled Bitcoin script for the payer's address.
|
||||
|
||||
### NAME_REGISTRATION
|
||||
|
||||
Op: `:`
|
||||
|
||||
Description: This transaction reveals the name whose hash was announced by a
|
||||
previous `NAME_PREORDER`. It is the second of two transactions that must be
|
||||
sent to register a name in BNS.
|
||||
|
||||
When this transaction confirms, the corresponding Blockstack DID will be
|
||||
instantiated. It's address will be the owner address in this transaction, and
|
||||
its index will be equal to the number of names registered to this address previously.
|
||||
|
||||
Example: [55b8b42fc3e3d23cbc0f07d38edae6a451dfc512b770fd7903725f9e465b2925](https://www.blocktrail.com/BTC/tx/55b8b42fc3e3d23cbc0f07d38edae6a451dfc512b770fd7903725f9e465b2925)
|
||||
|
||||
`OP_RETURN` wire format (2 variations allowed):
|
||||
|
||||
Variation 1:
|
||||
```
|
||||
0 2 3 39
|
||||
|----|--|-----------------------------|
|
||||
magic op name.ns_id (37 bytes)
|
||||
```
|
||||
|
||||
Variation 2:
|
||||
```
|
||||
0 2 3 39 59
|
||||
|----|--|----------------------------------|-------------------|
|
||||
magic op name.ns_id (37 bytes, 0-padded) value
|
||||
```
|
||||
|
||||
Inputs:
|
||||
* Payer `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
* `OP_RETURN` payload
|
||||
* `scriptPubkey` for the owner's address
|
||||
* `scriptPubkey` for the payer's change
|
||||
|
||||
Notes:
|
||||
|
||||
* Variation 1 simply registers the name. Variation 2 will register the name and
|
||||
set a name value simultaneously. This is used in practice to set a zone file
|
||||
hash for a name without the extra `NAME_UPDATE` transaction.
|
||||
* Both variations are supported. Variation 1 was designed for the time when
|
||||
Bitcoin only supported 40-byte `OP_RETURN` outputs.
|
||||
|
||||
### NAME_RENEWAL
|
||||
|
||||
Op: `:`
|
||||
|
||||
Description: This transaction renews a name in BNS. The name must still be
|
||||
registered and not expired, and owned by the transaction sender.
|
||||
|
||||
Depending on which namespace the name was created in, you may never need to
|
||||
renew a name. However, in namespaces where names expire (such as `.id`), you
|
||||
will need to renew your name periodically to continue using its associated DID.
|
||||
If this is a problem, we recommend creating a name in a namespace without name
|
||||
expirations, so that `NAME_UPDATE`, `NAME_TRANSFER` and `NAME_REVOKE` -- the operations that
|
||||
underpin the DID's operations -- will work indefinitely.
|
||||
|
||||
Example: [e543211b18e5d29fd3de7c0242cb017115f6a22ad5c6d51cf39e2b87447b7e65](https://www.blocktrail.com/BTC/tx/e543211b18e5d29fd3de7c0242cb017115f6a22ad5c6d51cf39e2b87447b7e65)
|
||||
|
||||
`OP_RETURN` wire format (2 variations allowed):
|
||||
|
||||
Variation 1:
|
||||
```
|
||||
0 2 3 39
|
||||
|----|--|-----------------------------|
|
||||
magic op name.ns_id (37 bytes)
|
||||
```
|
||||
|
||||
Variation 2:
|
||||
```
|
||||
0 2 3 39 59
|
||||
|----|--|----------------------------------|-------------------|
|
||||
magic op name.ns_id (37 bytes, 0-padded) value
|
||||
```
|
||||
|
||||
Inputs:
|
||||
|
||||
* Payer `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
|
||||
* `OP_RETURN` payload
|
||||
* `scriptPubkey` for the owner's addess. This can be a different address than
|
||||
the current name owner (in which case, the name is renewed and transferred).
|
||||
* `scriptPubkey` for the payer's change
|
||||
* `scriptPubkey` for the burn address (to pay the name cost)
|
||||
|
||||
Notes:
|
||||
|
||||
* This transaction is identical to a `NAME_REGISTRATION`, except for the presence of the fourth output that pays for the name cost (to the burn address).
|
||||
* Variation 1 simply renews the name. Variation 2 will both renew the name and
|
||||
set a new name value (in practice, the hash of a new zone file).
|
||||
* Both variations are supported. Variation 1 was designed for the time when
|
||||
Bitcoin only supported 40-byte `OP_RETURN` outputs.
|
||||
* This operation can be used to transfer a name to a new address by setting the
|
||||
second output (the first `scriptPubkey`) to be the `scriptPubkey` of the new
|
||||
owner key.
|
||||
|
||||
### NAME_UPDATE
|
||||
|
||||
Op: `+`
|
||||
|
||||
Description: This transaction sets the name state for a name to the given
|
||||
`value`. In practice, this is used to announce new DNS zone file hashes to the [Atlas
|
||||
network](https://docs.blockstack.org/core/atlas/overview.html), and in doing so,
|
||||
change where the name's off-chain state resides. In DID terminology, this
|
||||
operation changes where the authoritative replica of the DID's DDO will be
|
||||
retrieved on the DID's lookup.
|
||||
|
||||
Example: [e2029990fa75e9fc642f149dad196ac6b64b9c4a6db254f23a580b7508fc34d7](https://www.blocktrail.com/BTC/tx/e2029990fa75e9fc642f149dad196ac6b64b9c4a6db254f23a580b7508fc34d7)
|
||||
|
||||
`OP_RETURN` wire format:
|
||||
```
|
||||
0 2 3 19 39
|
||||
|-----|--|-----------------------------------|-----------------------|
|
||||
magic op hash128(name.ns_id,consensus hash) zone file hash
|
||||
```
|
||||
|
||||
Note that `hash128(name.ns_id, consensus hash)` is the first 16 bytes of a SHA256 hash over the name concatenated to the hexadecimal string of the consensus hash (not the bytes corresponding to that hex string).
|
||||
See the [Method Glossary](#method-glossary) below.
|
||||
|
||||
Example: `hash128("jude.id" + "8d8762c37d82360b84cf4d87f32f7754") == "d1062edb9ec9c85ad1aca6d37f2f5793"`.
|
||||
|
||||
The 20 byte zone file hash is computed from zone file data by using `ripemd160(sha56(zone file data))`
|
||||
|
||||
Inputs:
|
||||
* owner `scriptSig`
|
||||
* payment `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
* `OP_RETURN` payload
|
||||
* owner's `scriptPubkey`
|
||||
* payment `scriptPubkey` change
|
||||
|
||||
### NAME_TRANSFER
|
||||
|
||||
Op: `>`
|
||||
|
||||
Description: This transaction changes the public key hash that owns the name in
|
||||
BNS. When the name or its DID is looked up after this transaction confirms, the
|
||||
resolver will list the new public key as the owner.
|
||||
|
||||
Example: [7a0a3bb7d39b89c3638abc369c85b5c028d0a55d7804ba1953ff19b0125f3c24](https://www.blocktrail.com/BTC/tx/7a0a3bb7d39b89c3638abc369c85b5c028d0a55d7804ba1953ff19b0125f3c24)
|
||||
|
||||
`OP_RETURN` wire format:
|
||||
```
|
||||
0 2 3 4 20 36
|
||||
|-----|--|----|-------------------|---------------|
|
||||
magic op keep hash128(name.ns_id) consensus hash
|
||||
data?
|
||||
```
|
||||
|
||||
Inputs:
|
||||
|
||||
* Owner `scriptSig`
|
||||
* Payment `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
|
||||
* `OP_RETURN` payload
|
||||
* new name owner's `scriptPubkey`
|
||||
* old name owner's `scriptPubkey`
|
||||
* payment `scriptPubkey` change
|
||||
|
||||
Notes:
|
||||
|
||||
* The `keep data?` byte controls whether or not the name's 20-byte value is preserved (i.e. whether or not the name's associated zone file is preserved across the transfer).
|
||||
This value is either `>` to preserve it, or `~` to delete it. If you're simply
|
||||
re-keying, you should use `>`. You should only use `~` if you want to
|
||||
simultaneously dissociate the name (and its DID) from its off-chain state, like
|
||||
the DID's DDO.
|
||||
|
||||
### NAME_REVOKE
|
||||
|
||||
Op: `~`
|
||||
|
||||
Description: This transaction destroys a registered name. Its name state value
|
||||
in BNS will be cleared, and no further transactions will be able to affect the
|
||||
name until it expires (if its namespace allows it to expire at all). Once
|
||||
confirmed, this transaction ensures that neither the name nor the DID will
|
||||
resolve to a DDO.
|
||||
|
||||
Example: [eb2e84a45cf411e528185a98cd5fb45ed349843a83d39fd4dff2de47adad8c8f](https://www.blocktrail.com/BTC/tx/eb2e84a45cf411e528185a98cd5fb45ed349843a83d39fd4dff2de47adad8c8f)
|
||||
|
||||
`OP_RETURN` wire format:
|
||||
```
|
||||
0 2 3 39
|
||||
|----|--|-----------------------------|
|
||||
magic op name.ns_id (37 bytes)
|
||||
```
|
||||
|
||||
Inputs:
|
||||
|
||||
* owner `scriptSig`
|
||||
* payment `scriptSig`'s
|
||||
|
||||
Outputs:
|
||||
|
||||
* `OP_RETURN` payload
|
||||
* owner `scriptPubkey`
|
||||
* payment `scriptPubkey` change
|
||||
|
||||
## Method Glossary
|
||||
|
||||
Some hashing primitives are used to construct the wire-format representation of each name operation. They are enumerated here:
|
||||
|
||||
```
|
||||
B40_REGEX = '^[a-z0-9\-_.+]*$'
|
||||
|
||||
def is_b40(s):
|
||||
return isinstance(s, str) and re.match(B40_REGEX, s) is not None
|
||||
|
||||
def b40_to_bin(s):
|
||||
if not is_b40(s):
|
||||
raise ValueError('{} must only contain characters in the b40 char set'.format(s))
|
||||
return unhexlify(charset_to_hex(s, B40_CHARS))
|
||||
|
||||
def hexpad(x):
|
||||
return ('0' * (len(x) % 2)) + x
|
||||
|
||||
def charset_to_hex(s, original_charset):
|
||||
return hexpad(change_charset(s, original_charset, B16_CHARS))
|
||||
|
||||
def bin_hash160(s, hex_format=False):
|
||||
""" s is in hex or binary format
|
||||
"""
|
||||
if hex_format and is_hex(s):
|
||||
s = unhexlify(s)
|
||||
return hashlib.new('ripemd160', bin_sha256(s)).digest()
|
||||
|
||||
def hex_hash160(s, hex_format=False):
|
||||
""" s is in hex or binary format
|
||||
"""
|
||||
if hex_format and is_hex(s):
|
||||
s = unhexlify(s)
|
||||
return hexlify(bin_hash160(s))
|
||||
|
||||
def hash_name(name, script_pubkey, register_addr=None):
|
||||
"""
|
||||
Generate the hash over a name and hex-string script pubkey.
|
||||
Returns the hex-encoded string RIPEMD160(SHA256(x)), where
|
||||
x is the byte string composed of the concatenation of the
|
||||
binary
|
||||
"""
|
||||
bin_name = b40_to_bin(name)
|
||||
name_and_pubkey = bin_name + unhexlify(script_pubkey)
|
||||
|
||||
if register_addr is not None:
|
||||
name_and_pubkey += str(register_addr)
|
||||
|
||||
# make hex-encoded hash
|
||||
return hex_hash160(name_and_pubkey)
|
||||
|
||||
def hash128(data):
|
||||
"""
|
||||
Hash a string of data by taking its 256-bit sha256 and truncating it to the
|
||||
first 16 bytes
|
||||
"""
|
||||
return hexlify(bin_sha256(data)[0:16])
|
||||
```
|
||||
|
||||
@@ -68,7 +68,7 @@ def restore( working_dir, snapshot_path, restore_dir, pubkeys, num_required ):
|
||||
return False
|
||||
|
||||
# database must be identical
|
||||
db_filenames = ['blockstack-server.db', 'blockstack-server.snapshots', 'atlas.db', 'subdomains.db']
|
||||
db_filenames = ['blockstack-server.db', 'blockstack-server.snapshots', 'atlas.db', 'subdomains.db', 'subdomains.db.queue']
|
||||
src_paths = [os.path.join(working_dir, fn) for fn in db_filenames]
|
||||
backup_paths = [os.path.join(restore_dir, fn) for fn in db_filenames]
|
||||
|
||||
@@ -94,12 +94,6 @@ def restore( working_dir, snapshot_path, restore_dir, pubkeys, num_required ):
|
||||
print 'Missing import keychain {}'.format(import_keychain_path)
|
||||
return False
|
||||
|
||||
# all subdomains are present
|
||||
subds = ['bar.foo_{}.test'.format(i) for i in range(0,10)]
|
||||
subdomain_db = blockstack.lib.subdomains.SubdomainDB(os.path.join(restore_dir, 'subdomains.db'), os.path.join(restore_dir, 'zonefiles'))
|
||||
for subd in subds:
|
||||
rec = subdomain_db.get_subdomain_entry(subd)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -150,119 +144,190 @@ def scenario( wallets, **kw ):
|
||||
|
||||
testlib.next_block( **kw )
|
||||
|
||||
# propagate
|
||||
for name in zonefiles:
|
||||
# propagate the first five subdomains
|
||||
for i in range(0,5):
|
||||
name = 'foo_{}.test'.format(i)
|
||||
assert testlib.blockstack_put_zonefile(zonefiles[name])
|
||||
|
||||
# process subdomains
|
||||
|
||||
# process the first five subdomains
|
||||
testlib.next_block( **kw )
|
||||
|
||||
# propagate the last five subdomains, but don't process them
|
||||
for i in range(5,10):
|
||||
name = 'foo_{}.test'.format(i)
|
||||
assert testlib.blockstack_put_zonefile(zonefiles[name])
|
||||
|
||||
print 'waiting for all zone files to replicate'
|
||||
time.sleep(10)
|
||||
|
||||
working_dir = os.environ.get('BLOCKSTACK_WORKING_DIR')
|
||||
restore_dir = os.path.join(working_dir, "snapshot_dir")
|
||||
|
||||
# snapshot the latest backup
|
||||
snapshot_path = os.path.join(working_dir, "snapshot.bsk" )
|
||||
rc = blockstack.fast_sync_snapshot(kw['working_dir'], snapshot_path, wallets[3].privkey, None )
|
||||
if not rc:
|
||||
print "Failed to fast_sync_snapshot"
|
||||
return False
|
||||
|
||||
if not os.path.exists(snapshot_path):
|
||||
print "Failed to create snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
# make a backup
|
||||
db = testlib.get_state_engine()
|
||||
|
||||
# sign with more keys
|
||||
for i in xrange(4, 6):
|
||||
rc = blockstack.fast_sync_sign_snapshot( snapshot_path, wallets[i].privkey )
|
||||
print 'begin make backups of state from {}'.format(testlib.get_current_block(**kw) - 1)
|
||||
for name in os.listdir(os.path.join(working_dir, 'backups')):
|
||||
if name.endswith('.{}'.format(testlib.get_current_block(**kw) - 1)):
|
||||
os.unlink(os.path.join(working_dir, 'backups', name))
|
||||
|
||||
db.make_backups(testlib.get_current_block(**kw))
|
||||
print 'end make backups'
|
||||
|
||||
def _backup_and_restore():
|
||||
# snapshot the latest backup
|
||||
snapshot_path = os.path.join(working_dir, "snapshot.bsk" )
|
||||
rc = blockstack.fast_sync_snapshot(working_dir, snapshot_path, wallets[3].privkey, None )
|
||||
if not rc:
|
||||
print "Failed to sign with key {}".format(i)
|
||||
print "Failed to fast_sync_snapshot"
|
||||
return False
|
||||
|
||||
if not os.path.exists(snapshot_path):
|
||||
print "Failed to create snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
# restore!
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex, wallets[5].pubkey_hex], 3 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
# sign with more keys
|
||||
for i in xrange(4, 6):
|
||||
rc = blockstack.fast_sync_sign_snapshot( snapshot_path, wallets[i].privkey )
|
||||
if not rc:
|
||||
print "Failed to sign with key {}".format(i)
|
||||
return False
|
||||
|
||||
# restore!
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex, wallets[5].pubkey_hex], 3 )
|
||||
if not rc:
|
||||
print "1 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[5].pubkey_hex, wallets[4].pubkey_hex, wallets[3].pubkey_hex], 3 )
|
||||
if not rc:
|
||||
print "2 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "3 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[5].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "4 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[4].pubkey_hex, wallets[5].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "5 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "6 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[4].pubkey_hex, wallets[0].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "7 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[1].pubkey_hex, wallets[5].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "8 failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.move(restore_dir, restore_dir + '.bak')
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex], 2 )
|
||||
if rc:
|
||||
print "restored insufficient signatures snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex], 3 )
|
||||
if rc:
|
||||
print "restored insufficient signatures snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex], 1 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[3].pubkey_hex], 2 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[3].pubkey_hex, wallets[4].pubkey_hex], 3 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
shutil.move(restore_dir + '.bak', restore_dir)
|
||||
return True
|
||||
|
||||
# test backup and restore
|
||||
res = _backup_and_restore()
|
||||
if not res:
|
||||
return res
|
||||
|
||||
# first five subdomains are all present in the subdomain DB
|
||||
subds = ['bar.foo_{}.test'.format(i) for i in range(0,5)]
|
||||
subdomain_db = blockstack.lib.subdomains.SubdomainDB(os.path.join(restore_dir, 'subdomains.db'), os.path.join(restore_dir, 'zonefiles'))
|
||||
for subd in subds:
|
||||
rec = subdomain_db.get_subdomain_entry(subd)
|
||||
if not rec:
|
||||
print 'not found: {}'.format(subd)
|
||||
return False
|
||||
|
||||
# last 5 subdomains are queued in the subdomain DB queue
|
||||
queued_zfinfos = blockstack.lib.queue.queuedb_findall(os.path.join(restore_dir, 'subdomains.db.queue'), 'zonefiles')
|
||||
if len(queued_zfinfos) != 5:
|
||||
print 'only {} zonefiles queued'.format(queued_zfinfos)
|
||||
print queued_zfinfos
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[5].pubkey_hex, wallets[4].pubkey_hex, wallets[3].pubkey_hex], 3 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
# process the last five subdomains
|
||||
testlib.next_block( **kw )
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
shutil.rmtree(restore_dir)
|
||||
os.unlink(os.path.join(working_dir, "snapshot.bsk"))
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[5].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
# test backup and restore
|
||||
res = _backup_and_restore()
|
||||
if not res:
|
||||
return res
|
||||
|
||||
# all subdomains are all present in the subdomain DB
|
||||
subds = ['bar.foo_{}.test'.format(i) for i in range(0,10)]
|
||||
subdomain_db = blockstack.lib.subdomains.SubdomainDB(os.path.join(restore_dir, 'subdomains.db'), os.path.join(restore_dir, 'zonefiles'))
|
||||
for subd in subds:
|
||||
rec = subdomain_db.get_subdomain_entry(subd)
|
||||
if not rec:
|
||||
print 'not found: {}'.format(subd)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[4].pubkey_hex, wallets[5].pubkey_hex], 2 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[4].pubkey_hex, wallets[0].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[1].pubkey_hex, wallets[5].pubkey_hex], 1 )
|
||||
if not rc:
|
||||
print "failed to restore snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex], 2 )
|
||||
if rc:
|
||||
print "restored insufficient signatures snapshot {}".format(snapshot_path)
|
||||
# nothing queued
|
||||
queued_zfinfos = blockstack.lib.queue.queuedb_findall(os.path.join(restore_dir, 'subdomains.db.queue'), 'zonefiles')
|
||||
if len(queued_zfinfos) != 0:
|
||||
print '{} zonefiles queued'.format(queued_zfinfos)
|
||||
print queued_zfinfos
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[3].pubkey_hex, wallets[4].pubkey_hex], 3 )
|
||||
if rc:
|
||||
print "restored insufficient signatures snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex], 1 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[3].pubkey_hex], 2 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
# should fail
|
||||
rc = restore( kw['working_dir'], snapshot_path, restore_dir, [wallets[0].pubkey_hex, wallets[3].pubkey_hex, wallets[4].pubkey_hex], 3 )
|
||||
if rc:
|
||||
print "restored wrongly-signed snapshot {}".format(snapshot_path)
|
||||
return False
|
||||
|
||||
shutil.rmtree(restore_dir)
|
||||
|
||||
|
||||
def check( state_engine ):
|
||||
|
||||
# not revealed, but ready
|
||||
|
||||
@@ -2507,6 +2507,38 @@ def check_subdomain_db(firstblock=None, **kw):
|
||||
|
||||
assert subrec_did == subrec, 'At ({}, {}): Did not resolve to {}, but instead to {}'.format(subd, addr, subrec, subrec_did)
|
||||
|
||||
# make sure we can get all historic states of each subdomain
|
||||
for subd in all_subdomains:
|
||||
p = subprocess.Popen('sqlite3 "{}" \'select txid,accepted from subdomain_records where fully_qualified_subdomain = "{}" order by parent_zonefile_index, zonefile_offset\''
|
||||
.format(blockstack_opts['subdomaindb_path'], subd), shell=True, stdout=subprocess.PIPE)
|
||||
|
||||
all_txids_and_accepted, _ = p.communicate()
|
||||
|
||||
all_txids_and_accepted = all_txids_and_accepted.strip().split('\n')
|
||||
all_txids_and_accepted = [tuple(ataa.strip().split('|')) for ataa in all_txids_and_accepted]
|
||||
|
||||
assert len(all_txids_and_accepted) > 0, 'no subdomain rows for {}'.format(subd)
|
||||
|
||||
for txid_and_accepted in all_txids_and_accepted:
|
||||
txid = txid_and_accepted[0]
|
||||
accepted = txid_and_accepted[1]
|
||||
|
||||
res = requests.get('http://localhost:16268/v1/subdomains/{}'.format(txid))
|
||||
items = res.json()
|
||||
zfh = None
|
||||
|
||||
for (i, subdomain_op) in enumerate(items):
|
||||
assert subdomain_op['txid'] == txid, subdomain_op
|
||||
assert subdomain_op['zonefile_offset'] >= i, subdomain_op
|
||||
assert subdomain_op['accepted'] == int(accepted), subdomain_op
|
||||
assert subdomain_op['domain'] == subd.split('.', 1)[-1], subdomain_op
|
||||
|
||||
if zfh is None:
|
||||
zfh = subdomain_op['parent_zonefile_hash']
|
||||
|
||||
assert zfh == subdomain_op['parent_zonefile_hash'], subdomain_op
|
||||
|
||||
|
||||
print '\nend auditing the subdomain db\n'
|
||||
|
||||
return True
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__='0.20.0.0'
|
||||
__version__='20.0.0.0'
|
||||
|
||||
@@ -1,57 +1,62 @@
|
||||
FROM ubuntu:xenial
|
||||
FROM ubuntu:bionic
|
||||
WORKDIR /src/blockstack
|
||||
|
||||
# Install dependancies from apt
|
||||
RUN apt-get -y update
|
||||
RUN apt-get install -y python-pip python-dev libssl-dev libffi-dev rng-tools libgmp3-dev lsof
|
||||
|
||||
# Install Bitcoin
|
||||
RUN apt-get -y update
|
||||
RUN apt-get install -y python-software-properties
|
||||
RUN apt-get install -y software-properties-common
|
||||
# Install dependencies from apt
|
||||
RUN apt-get -y update && \
|
||||
apt-get install -y python-pip python-dev libssl-dev libffi-dev \
|
||||
rng-tools libgmp3-dev lsof wget curl apt-utils git gnupg sqlite3 \
|
||||
software-properties-common
|
||||
|
||||
# We need bitcoind
|
||||
RUN add-apt-repository ppa:bitcoin/bitcoin
|
||||
RUN apt-get -y update
|
||||
RUN apt-get install -y bitcoind sqlite3 curl
|
||||
RUN apt-get install -y bitcoind
|
||||
|
||||
# Add standard username and password
|
||||
RUN mkdir ~/.bitcoin
|
||||
RUN echo "rpcuser=blockstack\nrpcpassword=blockstacksystem\nrpcbind=0.0.0.0\nrpcallowip=0.0.0.0/0\n" > ~/.bitcoin/bitcoin.conf
|
||||
|
||||
# Install NodeJS
|
||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
|
||||
RUN apt-get install -y nodejs
|
||||
# Install node
|
||||
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# Install requirements for the blockstack.js integration tests
|
||||
WORKDIR /src/
|
||||
RUN apt-get install -y git
|
||||
# RUN npm i -g browserify babel-cli
|
||||
ADD https://api.github.com/repos/blockstack/blockstack.js/git/refs/heads/master blockstack-js-version.json
|
||||
RUN git clone https://git@github.com/blockstack/blockstack.js.git
|
||||
RUN cd blockstack.js && npm i && npm run build && npm ln
|
||||
# Install node apps
|
||||
|
||||
# Install requirements for running the transaction broadcaster service
|
||||
# Blockstack.js
|
||||
ADD https://api.github.com/repos/blockstack/blockstack.js/git/refs/heads/master blockstackjs-version.json
|
||||
RUN cd /src/ && git clone https://github.com/blockstack/blockstack.js.git
|
||||
RUN cd /src/blockstack.js && npm i && npm run build && npm i . -g
|
||||
|
||||
# Transaction broadcaster
|
||||
ADD https://api.github.com/repos/blockstack/transaction-broadcaster/git/refs/heads/master broadcaster-version.json
|
||||
RUN git clone https://git@github.com/blockstack/transaction-broadcaster.git
|
||||
RUN cd transaction-broadcaster && npm i && npm ln blockstack && npm run build
|
||||
RUN cd /src/ && git clone https://github.com/blockstack/transaction-broadcaster.git
|
||||
RUN cd /src/transaction-broadcaster && npm i && npm run build && npm i . -g
|
||||
|
||||
# And requirements for running the subdomain registrar service
|
||||
ADD https://api.github.com/repos/blockstack/subdomain-registrar/git/refs/heads/master subdomain-version.json
|
||||
RUN git clone https://git@github.com/blockstack/subdomain-registrar.git
|
||||
RUN cd subdomain-registrar && npm i && npm ln blockstack && npm run build
|
||||
# CLI
|
||||
ADD https://api.github.com/repos/blockstack/cli-blockstack/git/refs/heads/master cli-version.json
|
||||
RUN cd /src/ && git clone https://github.com/blockstack/cli-blockstack.git
|
||||
RUN cd /src/cli-blockstack && npm i && npm ln blockstack && npm run build && npm i . -g
|
||||
|
||||
# Install pyparsing
|
||||
RUN pip install pyparsing
|
||||
# Gaia hub
|
||||
|
||||
ADD https://api.github.com/repos/blockstack/gaia/git/refs/heads/master gaia-hub-version.json
|
||||
RUN cd /src/ && git clone https://github.com/blockstack/gaia.git
|
||||
RUN cd /src/gaia/hub && npm i && npm run build && npm i . -g
|
||||
|
||||
# Subdomain registrar
|
||||
|
||||
ADD https://api.github.com/repos/blockstack/subdomain-registrar/git/refs/heads/master registrar-version.json
|
||||
RUN cd /src/ && git clone https://github.com/blockstack/subdomain-registrar
|
||||
RUN cd /src/subdomain-registrar && npm i && npm run build && npm i . -g
|
||||
|
||||
# Build blockstack first
|
||||
WORKDIR /src/blockstack
|
||||
|
||||
# Copy all files from the repo into the container
|
||||
COPY . .
|
||||
|
||||
# Upgrade pip and install pyparsing
|
||||
RUN pip install pyparsing
|
||||
|
||||
# Install Blockstack from source
|
||||
RUN pip install .
|
||||
|
||||
# Change into the tests directory
|
||||
WORKDIR /src/blockstack/integration_tests
|
||||
|
||||
RUN pip install .
|
||||
RUN pip install . --upgrade
|
||||
RUN pip install ./integration_tests --upgrade
|
||||
|
||||
Reference in New Issue
Block a user