Browse Source

Merge branch 'develop'

master
Neil Booth 8 years ago
parent
commit
9130044c3b
  1. 64
      README.rst
  2. 75
      lib/coins.py
  3. 16
      lib/util.py
  4. 36
      server/block_processor.py
  5. 12
      server/controller.py
  6. 32
      server/daemon.py
  7. 2
      server/version.py

64
README.rst

@ -16,6 +16,34 @@ Getting Started
See `docs/HOWTO.rst`_. See `docs/HOWTO.rst`_.
Features
========
- Efficient, lightweight reimplementation of electrum-server
- Efficient synchronization of bitcoin mainnet from Genesis. Recent
hardware should synchronize in well under 24 hours. The fastest
time to height 448k (mid January 2017) reported is under 4h 30m. On
the same hardware JElectrum would take around 4 days and
electrum-server probably around 1 month.
- The full Electrum protocol is implemented. The only exception is
the blockchain.address.get_proof RPC call, which is not used by
Electrum GUI clients, and can only be invoked from the command line.
- Various configurable means of controlling resource consumption and
handling denial of service attacks. These include maximum
connection counts, subscription limits per-connection and across all
connections, maximum response size, per-session bandwidth limits,
and session timeouts.
- Minimal resource usage once caught up and serving clients; tracking the
transaction mempool appears to be the most expensive part.
- Fully asynchronous processing of new blocks, mempool updates, and
client requests. Busy clients should not noticeably impede other
clients' requests and notifications, nor the processing of incoming
blocks and mempool updates.
- Daemon failover. More than one daemon can be specified, and
ElectrumX will failover round-robin style if the current one fails
for any reason.
- Coin abstraction makes compatible altcoin and testnet support easy.
Motivation Motivation
========== ==========
@ -45,34 +73,6 @@ that could easily be reused for those alts that are reasonably
compatible with Bitcoin. Such an abstraction is also useful for compatible with Bitcoin. Such an abstraction is also useful for
testnets. testnets.
Features
========
- The full Electrum protocol is implemented. The only exception is
the blockchain.address.get_proof RPC call, which is not used by
Electrum GUI clients, and can only be invoked from the command line.
- Efficient synchronization from Genesis. Recent hardware should
synchronize in well under 24 hours, possibly much faster for recent
CPUs or if you have an SSD. The fastest time to height 439k (mid
November 2016) reported is under 5 hours. For comparison, JElectrum
would take around 4 days, and electrum-server probably around 1
month, on the same hardware.
- Various configurable means of controlling resource consumption and
handling denial of service attacks. These include maximum
connection counts, subscription limits per-connection and across all
connections, maximum response size, per-session bandwidth limits,
and session timeouts.
- Minimal resource usage once caught up and serving clients; tracking the
transaction mempool appears to be the most expensive part.
- Fully asynchronous processing of new blocks, mempool updates, and
client requests. Busy clients should not noticeably impede other
clients' requests and notifications, nor the processing of incoming
blocks and mempool updates.
- Daemon failover. More than one daemon can be specified, and
ElectrumX will failover round-robin style if the current one fails
for any reason.
- Coin abstraction makes compatible altcoin and testnet support easy.
Implementation Implementation
============== ==============
@ -135,6 +135,14 @@ version prior to the release of 1.0.
ChangeLog ChangeLog
========= =========
Version 0.10.17
---------------
Minor upgrade
* added current daemon URL and uptime to getinfo RPC call
* altcoin cleanups / fixes (erasmospunk)
Version 0.10.16 Version 0.10.16
--------------- ---------------

75
lib/coins.py

@ -32,17 +32,18 @@ class CoinError(Exception):
class Coin(object): class Coin(object):
'''Base class of coin hierarchy.''' '''Base class of coin hierarchy.'''
REORG_LIMIT=200 REORG_LIMIT = 200
# Not sure if these are coin-specific # Not sure if these are coin-specific
RPC_URL_REGEX = re.compile('.+@[^:]+(:[0-9]+)?') RPC_URL_REGEX = re.compile('.+@[^:]+(:[0-9]+)?')
VALUE_PER_COIN = 100000000 VALUE_PER_COIN = 100000000
CHUNK_SIZE=2016 CHUNK_SIZE = 2016
IRC_SERVER = "irc.freenode.net" IRC_SERVER = "irc.freenode.net"
IRC_PORT = 6667 IRC_PORT = 6667
HASHX_LEN = 11 HASHX_LEN = 11
# Peer discovery # Peer discovery
PEER_DEFAULT_PORTS = {'t':'50001', 's':'50002'} PEER_DEFAULT_PORTS = {'t': '50001', 's': '50002'}
PEERS = [] PEERS = []
TX_COUNT_HEIGHT = 0
@classmethod @classmethod
def lookup_coin_class(cls, name, net): def lookup_coin_class(cls, name, net):
@ -98,11 +99,11 @@ class Coin(object):
@util.cachedproperty @util.cachedproperty
def address_handlers(cls): def address_handlers(cls):
return ScriptPubKey.PayToHandlers( return ScriptPubKey.PayToHandlers(
address = cls.P2PKH_address_from_hash160, address=cls.P2PKH_address_from_hash160,
script_hash = cls.P2SH_address_from_hash160, script_hash=cls.P2SH_address_from_hash160,
pubkey = cls.P2PKH_address_from_pubkey, pubkey=cls.P2PKH_address_from_pubkey,
unspendable = lambda : None, unspendable=lambda: None,
strange = lambda script: None, strange=lambda script: None,
) )
@classmethod @classmethod
@ -269,8 +270,8 @@ class Bitcoin(Coin):
P2PKH_VERBYTE = 0x00 P2PKH_VERBYTE = 0x00
P2SH_VERBYTE = 0x05 P2SH_VERBYTE = 0x05
WIF_BYTE = 0x80 WIF_BYTE = 0x80
GENESIS_HASH=('000000000019d6689c085ae165831e93' GENESIS_HASH = ('000000000019d6689c085ae165831e93'
'4ff763ae46a2a6c172b3f1b60a8ce26f') '4ff763ae46a2a6c172b3f1b60a8ce26f')
TX_COUNT = 156335304 TX_COUNT = 156335304
TX_COUNT_HEIGHT = 429972 TX_COUNT_HEIGHT = 429972
TX_PER_BLOCK = 1800 TX_PER_BLOCK = 1800
@ -302,15 +303,15 @@ class BitcoinTestnet(Bitcoin):
P2PKH_VERBYTE = 0x6f P2PKH_VERBYTE = 0x6f
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
GENESIS_HASH=('000000000933ea01ad0ee984209779ba' GENESIS_HASH = ('000000000933ea01ad0ee984209779ba'
'aec3ced90fa3f408719526f8d77f4943') 'aec3ced90fa3f408719526f8d77f4943')
REORG_LIMIT = 2000 REORG_LIMIT = 2000
TX_COUNT = 12242438 TX_COUNT = 12242438
TX_COUNT_HEIGHT = 1035428 TX_COUNT_HEIGHT = 1035428
TX_PER_BLOCK = 21 TX_PER_BLOCK = 21
IRC_PREFIX = "ET_" IRC_PREFIX = "ET_"
RPC_PORT = 18332 RPC_PORT = 18332
PEER_DEFAULT_PORTS = {'t':'51001', 's':'51002'} PEER_DEFAULT_PORTS = {'t': '51001', 's': '51002'}
PEERS = [ PEERS = [
'electrum.akinbo.org s t', 'electrum.akinbo.org s t',
'he36kyperp3kbuxu.onion s t', 'he36kyperp3kbuxu.onion s t',
@ -319,6 +320,7 @@ class BitcoinTestnet(Bitcoin):
'testnet.not.fyi s t', 'testnet.not.fyi s t',
] ]
class BitcoinTestnetSegWit(BitcoinTestnet): class BitcoinTestnetSegWit(BitcoinTestnet):
'''Bitcoin Testnet for Core bitcoind >= 0.13.1. '''Bitcoin Testnet for Core bitcoind >= 0.13.1.
@ -343,8 +345,8 @@ class Litecoin(Coin):
P2PKH_VERBYTE = 0x30 P2PKH_VERBYTE = 0x30
P2SH_VERBYTE = 0x05 P2SH_VERBYTE = 0x05
WIF_BYTE = 0xb0 WIF_BYTE = 0xb0
GENESIS_HASH=('12a765e31ffd4059bada1e25190f6e98' GENESIS_HASH = ('12a765e31ffd4059bada1e25190f6e98'
'c99d9714d334efa41a195a7e7e04bfe2') 'c99d9714d334efa41a195a7e7e04bfe2')
TX_COUNT = 8908766 TX_COUNT = 8908766
TX_COUNT_HEIGHT = 1105256 TX_COUNT_HEIGHT = 1105256
TX_PER_BLOCK = 10 TX_PER_BLOCK = 10
@ -361,7 +363,8 @@ class LitecoinTestnet(Litecoin):
P2PKH_VERBYTE = 0x6f P2PKH_VERBYTE = 0x6f
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
# Some details missing... GENESIS_HASH = ('f5ae71e26c74beacc88382716aced69c'
'ddf3dffff24f384e1808905e0188f68f')
# Source: namecoin.org # Source: namecoin.org
@ -374,9 +377,11 @@ class Namecoin(Coin):
P2PKH_VERBYTE = 0x34 P2PKH_VERBYTE = 0x34
P2SH_VERBYTE = 0x0d P2SH_VERBYTE = 0x0d
WIF_BYTE = 0xe4 WIF_BYTE = 0xe4
GENESIS_HASH = ('000000000062b72c5e2ceb45fbc8587e'
'807c155b0da735e6483dfba2f0a9c770')
class NamecoinTestnet(Coin): class NamecoinTestnet(Namecoin):
NAME = "Namecoin" NAME = "Namecoin"
SHORTNAME = "XNM" SHORTNAME = "XNM"
NET = "testnet" NET = "testnet"
@ -385,6 +390,7 @@ class NamecoinTestnet(Coin):
P2PKH_VERBYTE = 0x6f P2PKH_VERBYTE = 0x6f
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
# TODO add GENESIS_HASH
# For DOGE there is disagreement across sites like bip32.org and # For DOGE there is disagreement across sites like bip32.org and
@ -398,9 +404,11 @@ class Dogecoin(Coin):
P2PKH_VERBYTE = 0x1e P2PKH_VERBYTE = 0x1e
P2SH_VERBYTE = 0x16 P2SH_VERBYTE = 0x16
WIF_BYTE = 0x9e WIF_BYTE = 0x9e
GENESIS_HASH = ('1a91e3dace36e2be3bf030a65679fe82'
'1aa1d6ef92e7c9902eb318182c355691')
class DogecoinTestnet(Coin): class DogecoinTestnet(Dogecoin):
NAME = "Dogecoin" NAME = "Dogecoin"
SHORTNAME = "XDT" SHORTNAME = "XDT"
NET = "testnet" NET = "testnet"
@ -409,6 +417,8 @@ class DogecoinTestnet(Coin):
P2PKH_VERBYTE = 0x71 P2PKH_VERBYTE = 0x71
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xf1 WIF_BYTE = 0xf1
GENESIS_HASH = ('bb0a78264637406b6360aad926284d54'
'4d7049f45189db5664f3c4d07350559e')
# Source: https://github.com/dashpay/dash # Source: https://github.com/dashpay/dash
@ -436,6 +446,7 @@ class Dash(Coin):
import x11_hash import x11_hash
return x11_hash.getPoWHash(header) return x11_hash.getPoWHash(header)
class DashTestnet(Dash): class DashTestnet(Dash):
SHORTNAME = "tDASH" SHORTNAME = "tDASH"
NET = "testnet" NET = "testnet"
@ -462,8 +473,8 @@ class Argentum(Coin):
P2PKH_VERBYTE = 0x17 P2PKH_VERBYTE = 0x17
P2SH_VERBYTE = 0x05 P2SH_VERBYTE = 0x05
WIF_BYTE = 0x97 WIF_BYTE = 0x97
GENESIS_HASH=('88c667bc63167685e4e4da058fffdfe8' GENESIS_HASH = ('88c667bc63167685e4e4da058fffdfe8'
'e007e5abffd6855de52ad59df7bb0bb2') 'e007e5abffd6855de52ad59df7bb0bb2')
TX_COUNT = 2263089 TX_COUNT = 2263089
TX_COUNT_HEIGHT = 2050260 TX_COUNT_HEIGHT = 2050260
TX_PER_BLOCK = 2000 TX_PER_BLOCK = 2000
@ -473,14 +484,14 @@ class Argentum(Coin):
class ArgentumTestnet(Argentum): class ArgentumTestnet(Argentum):
SHORTNAME = "XRG" SHORTNAME = "XRG"
NET = "testnet" NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf") XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394") XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = 0x6f P2PKH_VERBYTE = 0x6f
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
REORG_LIMIT = 2000 REORG_LIMIT = 2000
class DigiByte(Coin): class DigiByte(Coin):
@ -492,8 +503,8 @@ class DigiByte(Coin):
P2PKH_VERBYTE = 0x1E P2PKH_VERBYTE = 0x1E
P2SH_VERBYTE = 0x05 P2SH_VERBYTE = 0x05
WIF_BYTE = 0x80 WIF_BYTE = 0x80
GENESIS_HASH=('7497ea1b465eb39f1c8f507bc877078f' GENESIS_HASH = ('7497ea1b465eb39f1c8f507bc877078f'
'e016d6fcb6dfad3a64c98dcc6e1e8496') 'e016d6fcb6dfad3a64c98dcc6e1e8496')
TX_COUNT = 1046018 TX_COUNT = 1046018
TX_COUNT_HEIGHT = 1435000 TX_COUNT_HEIGHT = 1435000
TX_PER_BLOCK = 1000 TX_PER_BLOCK = 1000
@ -509,8 +520,8 @@ class DigiByteTestnet(DigiByte):
P2PKH_VERBYTE = 0x6f P2PKH_VERBYTE = 0x6f
P2SH_VERBYTE = 0xc4 P2SH_VERBYTE = 0xc4
WIF_BYTE = 0xef WIF_BYTE = 0xef
GENESIS_HASH=('b5dca8039e300198e5fe7cd23bdd1728' GENESIS_HASH = ('b5dca8039e300198e5fe7cd23bdd1728'
'e2a444af34c447dbd0916fa3430a68c2') 'e2a444af34c447dbd0916fa3430a68c2')
IRC_PREFIX = "DET_" IRC_PREFIX = "DET_"
IRC_CHANNEL = "#electrum-dgb" IRC_CHANNEL = "#electrum-dgb"
RPC_PORT = 15022 RPC_PORT = 15022

16
lib/util.py

@ -57,12 +57,20 @@ class cachedproperty(object):
return value return value
def formatted_time(t): def formatted_time(t, sep=' '):
'''Return a number of seconds as a string in days, hours, mins and '''Return a number of seconds as a string in days, hours, mins and
secs.''' maybe secs.'''
t = int(t) t = int(t)
return '{:d}d {:02d}h {:02d}m {:02d}s'.format( fmts = (('{:d}d', 86400), ('{:02d}h', 3600), ('{:02d}m', 60))
t // 86400, (t % 86400) // 3600, (t % 3600) // 60, t % 60) parts = []
for fmt, n in fmts:
val = t // n
if parts or val:
parts.append(fmt.format(val))
t %= n
if len(parts) < 3:
parts.append('{:02d}s'.format(t))
return sep.join(parts)
def deep_getsizeof(obj): def deep_getsizeof(obj):

36
server/block_processor.py

@ -383,26 +383,32 @@ class BlockProcessor(server.db.DB):
# Catch-up stats # Catch-up stats
if self.utxo_db.for_sync: if self.utxo_db.for_sync:
daemon_height = self.daemon.cached_height()
tx_per_sec = int(self.tx_count / self.wall_time) tx_per_sec = int(self.tx_count / self.wall_time)
this_tx_per_sec = 1 + int(tx_diff / (self.last_flush - last_flush)) this_tx_per_sec = 1 + int(tx_diff / (self.last_flush - last_flush))
if self.height > self.coin.TX_COUNT_HEIGHT:
tx_est = (daemon_height - self.height) * self.coin.TX_PER_BLOCK
else:
tx_est = ((daemon_height - self.coin.TX_COUNT_HEIGHT)
* self.coin.TX_PER_BLOCK
+ (self.coin.TX_COUNT - self.tx_count))
# Damp the enthusiasm
realism = 2.0 - 0.9 * self.height / self.coin.TX_COUNT_HEIGHT
tx_est *= max(realism, 1.0)
self.logger.info('tx/sec since genesis: {:,d}, ' self.logger.info('tx/sec since genesis: {:,d}, '
'since last flush: {:,d}' 'since last flush: {:,d}'
.format(tx_per_sec, this_tx_per_sec)) .format(tx_per_sec, this_tx_per_sec))
self.logger.info('sync time: {} ETA: {}' if self.coin.TX_COUNT_HEIGHT > 0:
.format(formatted_time(self.wall_time), daemon_height = self.daemon.cached_height()
formatted_time(tx_est / this_tx_per_sec))) if self.height > self.coin.TX_COUNT_HEIGHT:
tx_est = (daemon_height - self.height) * self.coin.TX_PER_BLOCK
else:
tx_est = ((daemon_height - self.coin.TX_COUNT_HEIGHT)
* self.coin.TX_PER_BLOCK
+ (self.coin.TX_COUNT - self.tx_count))
# Damp the enthusiasm
realism = 2.0 - 0.9 * self.height / self.coin.TX_COUNT_HEIGHT
tx_est *= max(realism, 1.0)
self.logger.info('sync time: {} ETA: {}'
.format(formatted_time(self.wall_time),
formatted_time(tx_est / this_tx_per_sec)))
else:
self.logger.info('sync time: {}'
.format(formatted_time(self.wall_time)))
def fs_flush(self): def fs_flush(self):
'''Flush the things stored on the filesystem.''' '''Flush the things stored on the filesystem.'''

12
server/controller.py

@ -450,6 +450,7 @@ class Controller(util.LoggedClass):
def getinfo(self): def getinfo(self):
'''A one-line summary of server state.''' '''A one-line summary of server state.'''
return { return {
'daemon': self.daemon.logged_url(),
'daemon_height': self.daemon.cached_height(), 'daemon_height': self.daemon.cached_height(),
'db_height': self.bp.db_height, 'db_height': self.bp.db_height,
'closing': len([s for s in self.sessions if s.is_closing()]), 'closing': len([s for s in self.sessions if s.is_closing()]),
@ -463,6 +464,7 @@ class Controller(util.LoggedClass):
'sessions': self.session_count(), 'sessions': self.session_count(),
'subs': self.sub_count(), 'subs': self.sub_count(),
'txs_sent': self.txs_sent, 'txs_sent': self.txs_sent,
'uptime': util.formatted_time(time.time() - self.start_time),
} }
def sub_count(self): def sub_count(self):
@ -514,12 +516,6 @@ class Controller(util.LoggedClass):
'''A generator returning lines for a list of sessions. '''A generator returning lines for a list of sessions.
data is the return value of rpc_sessions().''' data is the return value of rpc_sessions().'''
def time_fmt(t):
t = int(t)
return ('{:3d}:{:02d}:{:02d}'
.format(t // 3600, (t % 3600) // 60, t % 60))
fmt = ('{:<6} {:<5} {:>17} {:>5} {:>5} ' fmt = ('{:<6} {:<5} {:>17} {:>5} {:>5} '
'{:>7} {:>7} {:>7} {:>7} {:>7} {:>9} {:>21}') '{:>7} {:>7} {:>7} {:>7} {:>7} {:>9} {:>21}')
yield fmt.format('ID', 'Flags', 'Client', 'Reqs', 'Txs', 'Subs', yield fmt.format('ID', 'Flags', 'Client', 'Reqs', 'Txs', 'Subs',
@ -534,7 +530,7 @@ class Controller(util.LoggedClass):
'{:,d}'.format(recv_size // 1024), '{:,d}'.format(recv_size // 1024),
'{:,d}'.format(send_count), '{:,d}'.format(send_count),
'{:,d}'.format(send_size // 1024), '{:,d}'.format(send_size // 1024),
time_fmt(time), peer) util.formatted_time(time, sep=''), peer)
def session_data(self, for_log): def session_data(self, for_log):
'''Returned to the RPC 'sessions' call.''' '''Returned to the RPC 'sessions' call.'''
@ -599,7 +595,7 @@ class Controller(util.LoggedClass):
self.daemon.set_urls(self.env.coin.daemon_urls(daemon_url)) self.daemon.set_urls(self.env.coin.daemon_urls(daemon_url))
except Exception as e: except Exception as e:
raise RPCError('an error occured: {}'.format(e)) raise RPCError('an error occured: {}'.format(e))
return 'set daemon URL to {}'.format(daemon_url) return 'now using daemon at {}'.format(self.daemon.logged_url())
def rpc_stop(self): def rpc_stop(self):
'''Shut down the server cleanly.''' '''Shut down the server cleanly.'''

32
server/daemon.py

@ -42,10 +42,27 @@ class Daemon(util.LoggedClass):
'''Set the URLS to the given list, and switch to the first one.''' '''Set the URLS to the given list, and switch to the first one.'''
if not urls: if not urls:
raise DaemonError('no daemon URLs provided') raise DaemonError('no daemon URLs provided')
for url in urls:
self.logger.info('daemon at {}'.format(self.logged_url(url)))
self.urls = urls self.urls = urls
self.url_index = 0 self.url_index = 0
for n, url in enumerate(urls):
self.logger.info('daemon #{:d} at {}{}'
.format(n + 1, self.logged_url(url),
'' if n else ' (current)'))
def url(self):
'''Returns the current daemon URL.'''
return self.urls[self.url_index]
def failover(self):
'''Call to fail-over to the next daemon URL.
Returns False if there is only one, otherwise True.
'''
if len(self.urls) > 1:
self.url_index = (self.url_index + 1) % len(self.urls)
self.logger.info('failing over to {}'.format(self.logged_url()))
return True
return False
async def _send(self, payload, processor): async def _send(self, payload, processor):
'''Send a payload to be converted to JSON. '''Send a payload to be converted to JSON.
@ -72,8 +89,7 @@ class Daemon(util.LoggedClass):
while True: while True:
try: try:
async with self.workqueue_semaphore: async with self.workqueue_semaphore:
url = self.urls[self.url_index] async with aiohttp.post(self.url(), data=data) as resp:
async with aiohttp.post(url, data=data) as resp:
# If bitcoind can't find a tx, for some reason # If bitcoind can't find a tx, for some reason
# it returns 500 but fills out the JSON. # it returns 500 but fills out the JSON.
# Should still return 200 IMO. # Should still return 200 IMO.
@ -99,17 +115,15 @@ class Daemon(util.LoggedClass):
except Exception: except Exception:
self.log_error(traceback.format_exc()) self.log_error(traceback.format_exc())
if secs >= max_secs and len(self.urls) > 1: if secs >= max_secs and self.failover():
self.url_index = (self.url_index + 1) % len(self.urls)
logged_url = self.logged_url(self.urls[self.url_index])
self.logger.info('failing over to {}'.format(logged_url))
secs = 1 secs = 1
else: else:
await asyncio.sleep(secs) await asyncio.sleep(secs)
secs = min(max_secs, secs * 2) secs = min(max_secs, secs * 2)
def logged_url(self, url): def logged_url(self, url=None):
'''The host and port part, for logging.''' '''The host and port part, for logging.'''
url = url or self.url()
return url[url.rindex('@') + 1:] return url[url.rindex('@') + 1:]
async def _send_single(self, method, params=None): async def _send_single(self, method, params=None):

2
server/version.py

@ -1 +1 @@
VERSION = "ElectrumX 0.10.16" VERSION = "ElectrumX 0.10.17"

Loading…
Cancel
Save