Rework electrumx_rpc; add "query" command

This commit is contained in:
Neil Booth 2018-08-02 15:15:08 +09:00
parent 147989a0a6
commit 9185198703
4 changed files with 182 additions and 31 deletions

View File

@ -23,7 +23,7 @@ Add a peer to the peers list. ElectrumX will schdule an immediate
connection attempt. This command takes a single argument: the peer's
"real name" as it used to advertise itself on IRC::
$ ./electrumx_rpc add_peer "ecdsa.net v1.0 s110 t"
$ electrumx_rpc add_peer "ecdsa.net v1.0 s110 t"
"peer 'ecdsa.net v1.0 s110 t' added"
daemon_url
@ -48,7 +48,7 @@ disconnect
Disconnect the given session IDs. Session IDs can be seen in the logs
or with the `sessions`_ RPC command::
$ ./electrumx_rpc disconnect 2 3
$ electrumx_rpc disconnect 2 3
[
"disconnected 2",
"disconnected 3"
@ -161,6 +161,30 @@ Peer data is obtained via a peer discovery protocol documented
[...]
electroncash.checksum0.com good 50001 50002 ElectrumX 1.2.1 0.9 1.1 07h 30m 40s 07h 30m 41s 0 peer 149.56.198.233
query
-----
Run a query of the UTXO and history databases against one or more
addresses or hex scripts. `--limit <N>` or `-l <N>` limits the output
for each kind to that many entries. History is printed in blockchain
order; UTXOs in an arbitrary order.
For example::
$ electrumx_rpc query --limit 5 76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac
Script: 76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac
History #1: height 123,723 tx_hash 3387418aaddb4927209c5032f515aa442a6587d6e54677f08a03b8fa7789e688
History #2: height 127,280 tx_hash 4574958d135e66a53abf9c61950aba340e9e140be50efeea9456aa9f92bf40b5
History #3: height 127,909 tx_hash 8b960c87f9f1a6e6910e214fcf5f9c69b60319ba58a39c61f299548412f5a1c6
History #4: height 127,943 tx_hash 8f6b63012753005236b1b76e4884e4dee7415e05ab96604d353001662cde6b53
History #5: height 127,943 tx_hash 60ff2dfdf67917040139903a0141f7525a7d152365b371b35fd1cf83f1d7f704
UTXO #1: tx_hash 9aa497bf000b20f5ec5dc512bb6c1b60b68fc584d38b292b434e839ea8807bf0 tx_pos 0 height 254,148 value 5,500
UTXO #2: tx_hash 1c998142a5a5aae6f8c1eab245351413fe8d4032a3f14345f9943a0d0bc90ec0 tx_pos 0 height 254,161 value 5,500
UTXO #3: tx_hash 53345491b4829140be53f30079c6e4556a18545343b122900ebbfa158f9ca97a tx_pos 0 height 254,163 value 5,500
UTXO #4: tx_hash c71ad947ac46af217da3cd5521113cbd03e36ddada2b4452afe6c15f944d2529 tx_pos 0 height 372,916 value 1,000
UTXO #5: tx_hash c944a6acac054275a5e294e746d9ce79f6dcae91f3b4f5a84561aee6404a55b3 tx_pos 0 height 254,148 value 5,500
Balance: 17.8983303 BCH
reorg
-----

View File

@ -11,6 +11,8 @@ import pylru
from aiorpcx import run_in_thread
from electrumx.lib.hash import hash_to_hex_str
class ChainState(object):
'''Used as an interface by servers to request information about
@ -92,3 +94,50 @@ class ChainState(object):
def set_daemon_url(self, daemon_url):
self._daemon.set_urls(self._env.coin.daemon_urls(daemon_url))
return self._daemon.logged_url()
async def query(self, args, limit):
coin = self._env.coin
db = self._bp
lines = []
def arg_to_hashX(arg):
try:
script = bytes.fromhex(arg)
lines.append(f'Script: {arg}')
return coin.hashX_from_script(script)
except ValueError:
pass
try:
hashX = coin.address_to_hashX(arg)
lines.append(f'Address: {arg}')
return hashX
except Base58Error:
print(f'Ingoring unknown arg: {arg}')
return None
for arg in args:
hashX = arg_to_hashX(arg)
if not hashX:
continue
n = None
for n, (tx_hash, height) in enumerate(
db.get_history(hashX, limit), start=1):
lines.append(f'History #{n:,d}: height {height:,d} '
f'tx_hash {hash_to_hex_str(tx_hash)}')
if n is None:
lines.append('No history found')
n = None
for n, utxo in enumerate(db.get_utxos(hashX, limit), start=1):
lines.append(f'UTXO #{n:,d}: tx_hash '
f'{hash_to_hex_str(utxo.tx_hash)} '
f'tx_pos {utxo.tx_pos:,d} height {utxo.height:,d} '
f'value {utxo.value:,d}')
if n is None:
lines.append('No UTXOs found')
balance = db.get_balance(hashX)
lines.append(f'Balance: {coin.decimal_value(balance):,f} '
f'{coin.SHORTNAME}')
return lines

View File

@ -130,7 +130,7 @@ class SessionManager(object):
# Set up the RPC request handlers
cmds = ('add_peer daemon_url disconnect getinfo groups log peers '
'reorg sessions stop'.split())
'query reorg sessions stop'.split())
self.rpc_handlers = {cmd: getattr(self, 'rpc_' + cmd) for cmd in cmds}
async def _start_server(self, kind, *args, **kw_args):
@ -353,7 +353,7 @@ class SessionManager(object):
return self._for_each_session(session_ids, toggle_logging)
def rpc_daemon_url(self, daemon_url=None):
def rpc_daemon_url(self, daemon_url):
'''Replace the daemon URL.'''
daemon_url = daemon_url or self.env.daemon_url
try:
@ -379,19 +379,23 @@ class SessionManager(object):
'''Return a list of data about server peers.'''
return self.peer_mgr.rpc_data()
async def rpc_query(self, items, limit):
'''Return a list of data about server peers.'''
return await self.chain_state.query(items, limit)
def rpc_sessions(self):
'''Return statistics about connected sessions.'''
return self._session_data(for_log=False)
def rpc_reorg(self, count=3):
def rpc_reorg(self, count):
'''Force a reorg of the given number of blocks.
count: number of blocks to reorg (default 3)
count: number of blocks to reorg
'''
count = non_negative_integer(count)
if not self.chain_state.force_chain_reorg(count):
raise RPCError(BAD_REQUEST, 'still catching up with daemon')
return 'scheduled a reorg of {:,d} blocks'.format(count)
return f'scheduled a reorg of {count:,d} blocks'
# --- External Interface

View File

@ -10,6 +10,7 @@
'''Script to send RPC commands to a running ElectrumX server.'''
from aiorpcx import timeout_after
import argparse
import asyncio
import json
@ -19,38 +20,111 @@ import electrumx.lib.text as text
from aiorpcx import ClientSession
simple_commands = {
'getinfo': 'Print a summary of server state',
'groups': 'Print current session groups',
'peers': 'Print information about peer servers for the same coin',
'sessions': 'Print information about client sessions',
'stop': 'Shut down the server cleanly',
}
session_commands = {
'disconnect': 'Disconnect sessions',
'log': 'Toggle logging of sessions',
}
other_commands = {
'add_peer': (
'add a peer to the peers list',
[], {
'type': str,
'dest': 'real_name',
'help': 'e.g. "a.domain.name s995 t"',
},
),
'daemon_url': (
"replace the daemon's URL at run-time, and forecefully rotate "
" to the first URL in the list",
[], {
'type': str,
'nargs': '?',
'default': '',
'dest': 'daemon_url',
'help': 'see documentation of DAEMON_URL envvar',
},
),
'query': (
'query the UTXO and history databases',
['-l', '--limit'], {
'type': int,
'default': 1000,
'help': 'UTXO and history output limit',
}, ['items'], {
'nargs': '+',
'type': str,
'help': 'hex scripts, or addresses, to query',
},
),
'reorg': (
'simulate a chain reorganization',
[], {
'type': int,
'dest': 'count',
'default': 3,
'help': 'number of blocks to back up'
},
),
}
def main():
'''Send the RPC command to the server and print the result.'''
parser = argparse.ArgumentParser('Send electrumx an RPC command')
parser.add_argument('-p', '--port', metavar='port_num', type=int,
help='RPC port number')
parser.add_argument('command', nargs=1, default=[],
help='command to send')
parser.add_argument('param', nargs='*', default=[],
help='params to send')
args = parser.parse_args()
main_parser = argparse.ArgumentParser(
'elextrumx_rpc',
description='Send electrumx an RPC command'
)
main_parser.add_argument('-p', '--port', metavar='port_num', type=int,
help='RPC port number')
port = args.port
subparsers = main_parser.add_subparsers(help='sub-command help',
dest='command')
for command, help in simple_commands.items():
parser = subparsers.add_parser(command, help=help)
for command, help in session_commands.items():
parser = subparsers.add_parser(command, help=help)
parser.add_argument('session_ids', nargs='+', type=int,
help='list of session ids')
for command, data in other_commands.items():
parser_help, *arguments = data
parser = subparsers.add_parser(command, help=parser_help)
for n in range(0, len(arguments), 2):
args, kwargs = arguments[n: n+2]
parser.add_argument(*args, **kwargs)
args = main_parser.parse_args()
args = vars(args)
port = args.pop('port')
if port is None:
port = int(environ.get('RPC_PORT', 8000))
method = args.pop('command')
# Get the RPC request.
method = args.command[0]
params = args.param
if method in ('log', 'disconnect'):
params = [params]
# aiorpcX makes this so easy...
async def send_request():
# aiorpcX makes this so easy...
async with ClientSession('localhost', port) as session:
result = await session.send_request(method, params, timeout=15)
if method in ('groups', 'peers', 'sessions'):
lines_func = getattr(text, f'{method}_lines')
for line in lines_func(result):
print(line)
else:
print(json.dumps(result, indent=4, sort_keys=True))
async with timeout_after(15):
async with ClientSession('localhost', port) as session:
result = await session.send_request(method, args)
if method in ('query', ):
for line in result:
print(line)
elif method in ('groups', 'peers', 'sessions'):
lines_func = getattr(text, f'{method}_lines')
for line in lines_func(result):
print(line)
else:
print(json.dumps(result, indent=4, sort_keys=True))
loop = asyncio.get_event_loop()
try: