Initial revision
This commit is contained in:
commit
a3dbc68614
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*/__pycache__/
|
||||
*/*~
|
||||
*.#*
|
||||
*#
|
||||
*~
|
||||
19
ACKNOWLEDGEMENTS
Normal file
19
ACKNOWLEDGEMENTS
Normal file
@ -0,0 +1,19 @@
|
||||
Thanks to Thomas Voegtlin for creating the Electrum software and
|
||||
infrastructure and for maintaining it so diligently. Electrum is the
|
||||
probably the best desktop Bitcoin wallet solution for most users. My
|
||||
faith in it is such that I use Electrum software to store most of my
|
||||
Bitcoins.
|
||||
|
||||
Whilst the vast majority of the code here is my own original work and
|
||||
includes some new ideas, it is very clear that the general structure
|
||||
and concept are those of Electrum. Some parts of the code and ideas
|
||||
of Electrum, some of which it itself took from other projects such as
|
||||
Abe and pywallet, remain. Thanks to the authors of all the software
|
||||
this is derived from.
|
||||
|
||||
Thanks to Daniel Bernstein for daemontools and other software, and to
|
||||
Matthew Dillon for DragonFlyBSD. They are both deeply inspirational
|
||||
people.
|
||||
|
||||
And of course, thanks to Satoshi for the wonderful creation that is
|
||||
Bitcoin.
|
||||
267
HOWTO.rst
Normal file
267
HOWTO.rst
Normal file
@ -0,0 +1,267 @@
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
ElectrumX should run on any flavour of unix. I have run it
|
||||
successfully on MaxOSX and DragonFlyBSD. It won't run out-of-the-box
|
||||
on Windows, but the changes required to make it do so should be
|
||||
small - patches welcome.
|
||||
|
||||
+ Python3 ElectrumX makes heavy use of asyncio so version >=3.5 is required
|
||||
+ plyvel Python interface to LevelDB. I am using plyvel-0.9.
|
||||
+ aiohttp Python library for asynchronous HTTP. ElectrumX uses it for
|
||||
communication with the daemon. I am using aiohttp-0.21.
|
||||
|
||||
While not requirements for running ElectrumX, it is intended to be run
|
||||
with supervisor software such as Daniel Bernstein's daemontools, or
|
||||
Gerald Pape's runit package. These make administration of secure
|
||||
unix servers very easy, and I strongly recommend you install one of these
|
||||
and familiarise yourself with them. The instructions below and sample
|
||||
run scripts assume daemontools; adapting to runit should be trivial
|
||||
for someone used to either.
|
||||
|
||||
When building the database form the genesis block, ElectrumX has to
|
||||
flush large quantities of data to disk and to leveldb. You will have
|
||||
a much nicer experience if the database directory is on an SSD than on
|
||||
an HDD. Currently to around height 430,000 of the Bitcoin blockchain
|
||||
the final size of the leveldb database, and other ElectrumX file
|
||||
metadata comes to around 15GB. Leveldb needs a bit more for brief
|
||||
periods, and the block chain is only getting longer, so I would
|
||||
recommend having at least 30-40GB free space.
|
||||
|
||||
|
||||
Running
|
||||
=======
|
||||
|
||||
Install the prerequisites above.
|
||||
|
||||
Check out the code from Github::
|
||||
|
||||
git clone https://github.com/kyuupichan/electrumx.git
|
||||
cd electrumx
|
||||
|
||||
I have not yet created a setup.py, so for now I suggest you run
|
||||
the code from the source tree or a copy of it.
|
||||
|
||||
You should create a standard user account to run the server under;
|
||||
your own is probably adequate unless paranoid. The paranoid might
|
||||
also want to create another user account for the daemontools logging
|
||||
process. The sample scripts and these instructions assume it is all
|
||||
under one account which I have called 'electrumx'.
|
||||
|
||||
Next create a directory where the database will be stored and make it
|
||||
writeable by the electrumx account. I recommend this directory live
|
||||
on an SSD::
|
||||
|
||||
mkdir /path/to/db_directory
|
||||
chown electrumx /path/to/db_directory
|
||||
|
||||
Next create a daemontools service directory; this only holds symlinks
|
||||
(see daemontools documentation). The 'svscan' program will ensure the
|
||||
servers in the directory are running by launching a 'supervise'
|
||||
supervisor for the server and another for its logging process. You
|
||||
can run 'svscan' under the electrumx account if that is the only one
|
||||
involved (server and logger) otherwise it will need to run as root so
|
||||
that the user can be switched to electrumx.
|
||||
|
||||
Assuming this directory is called service, you would do one of::
|
||||
|
||||
mkdir /service # If running svscan as root
|
||||
mkdir ~/service # As electrumx if running svscan as that a/c
|
||||
|
||||
Next create a directory to hold the scripts that the 'supervise'
|
||||
process spawned by 'svscan' will run - this directory must be readable
|
||||
by the 'svscan' process. Suppose this directory is called scripts, you
|
||||
might do::
|
||||
|
||||
mkdir -p ~/scripts/electrumx
|
||||
|
||||
Then copy the all sample scripts from the ElectrumX source tree there::
|
||||
|
||||
cp -R /path/to/repo/electrumx/samples/scripts ~/scripts/electrumx
|
||||
|
||||
This copies 4 things: the top level server run script, a log/ directory
|
||||
with the logger run script, an env/ directory, and a NOTES file.
|
||||
|
||||
You need to configure the environment variables under env/ to your
|
||||
setup, as explained in NOTES. ElectrumX server currently takes no
|
||||
command line arguments; all of its configuration is taken from its
|
||||
environment which is set up according to env/ directory (see 'envdir'
|
||||
man page). Finally you need to change the log/run script to use the
|
||||
directory where you want the logs to be written by multilog. The
|
||||
directory need not exist as multilog will create it, but its parent
|
||||
directory must exist.
|
||||
|
||||
Now start the 'svscan' process. This will not do much as the service
|
||||
directory is still empty::
|
||||
|
||||
svscan ~/service & disown
|
||||
|
||||
svscan is now waiting for services to be added to the directory::
|
||||
|
||||
cd ~/service
|
||||
ln -s ~/scripts/electrumx electrumx
|
||||
|
||||
Creating the symlink will kick off the server process almost immediately.
|
||||
You can see its logs with::
|
||||
|
||||
tail -F /path/to/log/dir/current | tai64nlocal
|
||||
|
||||
|
||||
Progress
|
||||
========
|
||||
|
||||
Speed indexing the blockchain depends on your hardware of course. As
|
||||
Python is single-threaded most of the time only 1 core is kept busy.
|
||||
ElectrumX uses Python's asyncio to prefill a cache of future blocks
|
||||
asynchronously; this keeps the CPU busy processing the chain and not
|
||||
waiting for blocks to be delivered. I therefore doubt there will be
|
||||
much boost in performance if the daemon is on the same host: indeed it
|
||||
may even be beneficial to have the daemon on a separate machine so the
|
||||
machine doing the indexing is focussing on the one task and not the
|
||||
wider network.
|
||||
|
||||
The FLUSH_SIZE environment variable is an upper bound on how much
|
||||
unflushed data is cached before writing to disk + leveldb. The
|
||||
default is 4 million items, which is probably fine unless your
|
||||
hardware is quite poor. If you've got a really fat machine with lots
|
||||
of RAM, 10 million or even higher is likely good (I used 10 million on
|
||||
Machine B below without issue so far). A higher number will have
|
||||
fewer flushes and save your disk thrashing, but you don't want it so
|
||||
high your machine is swapping. If your machine loses power all
|
||||
synchronization since the previous flush is lost.
|
||||
|
||||
When syncing, ElectrumX is CPU bound over 70% of the time, with the
|
||||
rest being bursts of disk activity whilst flushing. Here is my
|
||||
experience with the current codebase, to given heights and rough
|
||||
wall-time::
|
||||
|
||||
Machine A Machine B DB + Metadata
|
||||
100,000 2m 30s 0 (unflushed)
|
||||
150,000 35m 4m 30s 0.2 GB
|
||||
180,000 1h 5m 9m 0.4 GB
|
||||
245,800 3h
|
||||
290,000 13h 15m 3.3 GB
|
||||
|
||||
Machine A: a low-spec 2011 1.6GHz AMD E-350 dual-core fanless CPU, 8GB
|
||||
RAM and a DragonFlyBSD HAMMER fileystem on an SSD. It requests blocks
|
||||
over the LAN from a bitcoind on machine B. FLUSH_SIZE: I changed it
|
||||
several times between 1 and 5 million during the sync which causes the
|
||||
above stats to be a little approximate. Initial FLUSH_SIZE was 1
|
||||
million and first flush at height 126,538.
|
||||
|
||||
Machine B: a late 2012 iMac running El-Capitan 10.11.6, 2.9GHz
|
||||
quad-core Intel i5 CPU with an HDD and 24GB RAM. Running bitcoind on
|
||||
the same machine. FLUSH_SIZE of 10 million. First flush at height
|
||||
195,146.
|
||||
|
||||
Transactions processed per second seems to gradually decrease over
|
||||
time but this statistic is not currently logged and I've not looked
|
||||
closely.
|
||||
|
||||
For chains other than bitcoin-mainnet sychronization should be much
|
||||
faster.
|
||||
|
||||
|
||||
Terminating ElectrumX
|
||||
=====================
|
||||
|
||||
The preferred way to terminate the server process is to send it the
|
||||
TERM signal. For a daemontools supervised process this is best done
|
||||
by bringing it down like so::
|
||||
|
||||
svc -d ~/service/electrumx
|
||||
|
||||
If processing the blockchain the server will start the process of
|
||||
flushing to disk. Once that is complete the server will exit. Be
|
||||
patient as disk flushing can take a while.
|
||||
|
||||
ElectrumX flushes to leveldb using its transaction functionality. The
|
||||
plyvel documentation claims this is atomic. I have written ElectrumX
|
||||
with the intent that, to the extent this atomicity guarantee holds,
|
||||
the database should not get corrupted even if the ElectrumX process if
|
||||
forcibly killed or there is loss of power. The worst case is losing
|
||||
unflushed in-memory blockchain processing and having to restart from
|
||||
the state as of the prior successfully completed flush.
|
||||
|
||||
During development I have terminated ElectrumX processes in various
|
||||
ways and at random times, and not once have I had any corruption as a
|
||||
result of doing so. Mmy only DB corruption has been through buggy
|
||||
code. If you do have any database corruption as a result of
|
||||
terminating the process without modifying the code I would be very
|
||||
interested in hearing details.
|
||||
|
||||
I have heard about corruption issues with electrum-server. I cannot
|
||||
be sure but with a brief look at the code it does seem that if
|
||||
interrupted at the wrong time the databases it uses could become
|
||||
inconsistent.
|
||||
|
||||
Once the process has terminated, you can start it up again with::
|
||||
|
||||
svc -u ~/service/electrumx
|
||||
|
||||
You can see the status of a running service with::
|
||||
|
||||
svstat ~/service/electrumx
|
||||
|
||||
Of course, svscan can handle multiple services simultaneously from the
|
||||
same service directory, such as a testnet or altcoin server. See the
|
||||
man pages of these various commands for more information.
|
||||
|
||||
|
||||
|
||||
Understanding the Logs
|
||||
======================
|
||||
|
||||
You can see the logs usefully like so::
|
||||
|
||||
tail -F /path/to/log/dir/current | tai64nlocal
|
||||
|
||||
Here is typical log output on startup::
|
||||
|
||||
|
||||
2016-10-08 14:46:48.088516500 Launching ElectrumX server...
|
||||
2016-10-08 14:46:49.145281500 INFO:root:ElectrumX server starting
|
||||
2016-10-08 14:46:49.147215500 INFO:root:switching current directory to /var/nohist/server-test
|
||||
2016-10-08 14:46:49.150765500 INFO:DB:using flush size of 1,000,000 entries
|
||||
2016-10-08 14:46:49.156489500 INFO:DB:created new database Bitcoin-mainnet
|
||||
2016-10-08 14:46:49.157531500 INFO:DB:flushing to levelDB 0 txs and 0 blocks to height -1 tx count: 0
|
||||
2016-10-08 14:46:49.158640500 INFO:DB:flushed. Cache hits: 0/0 writes: 5 deletes: 0 elided: 0 sync: 0d 00h 00m 00s
|
||||
2016-10-08 14:46:49.159508500 INFO:RPC:using RPC URL http://user:pass@192.168.0.2:8332/
|
||||
2016-10-08 14:46:49.167352500 INFO:BlockCache:catching up, block cache limit 10MB...
|
||||
2016-10-08 14:46:49.318374500 INFO:BlockCache:prefilled 10 blocks to height 10 daemon height: 433,401 block cache size: 2,150
|
||||
2016-10-08 14:46:50.193962500 INFO:BlockCache:prefilled 4,000 blocks to height 4,010 daemon height: 433,401 block cache size: 900,043
|
||||
2016-10-08 14:46:51.253644500 INFO:BlockCache:prefilled 4,000 blocks to height 8,010 daemon height: 433,401 block cache size: 1,600,613
|
||||
2016-10-08 14:46:52.195633500 INFO:BlockCache:prefilled 4,000 blocks to height 12,010 daemon height: 433,401 block cache size: 2,329,325
|
||||
|
||||
Under normal operation these prefill messages repeat fairly regularly.
|
||||
Occasionally (depending on how big your FLUSH_SIZE environment
|
||||
variable was set, and your hardware, this could be anything from every
|
||||
5 minutes to every hour) you will get a flush to disk that begins with:
|
||||
|
||||
2016-10-08 06:34:20.841563500 INFO:DB:flushing to levelDB 828,190 txs and 3,067 blocks to height 243,982 tx count: 20,119,669
|
||||
|
||||
During the flush, which can take many minutes, you may see logs like
|
||||
this:
|
||||
|
||||
2016-10-08 12:20:08.558750500 INFO:DB:address 1dice7W2AicHosf5EL3GFDUVga7TgtPFn hist moving to idx 3000
|
||||
|
||||
These are just informational messages about addresses that have very
|
||||
large histories that are generated as those histories are being
|
||||
written outt. After the flush has completed a few stats are printed
|
||||
about cache hits, the number of writes and deletes, and the number of
|
||||
writes that were elided by the cache::
|
||||
|
||||
2016-10-08 06:37:41.035139500 INFO:DB:flushed. Cache hits: 3,185,958/192,336 writes: 781,526 deletes: 465,236 elided: 3,185,958 sync: 0d 06h 57m 03s
|
||||
|
||||
After flush-to-disk you may see an aiohttp error; this is the daemon
|
||||
timing out the connection while the disk flush was in progress. This
|
||||
is harmless; I intend to fix this soon by yielding whilst flushing.
|
||||
|
||||
You may see one or two logs about ambiguous UTXOs or hash160s::
|
||||
|
||||
2016-10-08 07:24:34.068609500 INFO:DB:UTXO compressed key collision at height 252943 utxo 115cc1408e5321636675a8fcecd204661a6f27b4b7482b1b7c4402ca4b94b72f / 1
|
||||
|
||||
These are an informational message about artefact of the compression
|
||||
scheme ElectrumX uses and are harmless. However, if you see more than
|
||||
a handful of these, particularly close together, something is very
|
||||
wrong and your DB is probably corrupt.
|
||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
||||
Copyright (c) 2016, Neil Booth
|
||||
|
||||
All rights reserved.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
126
README.rst
Normal file
126
README.rst
Normal file
@ -0,0 +1,126 @@
|
||||
ElectrumX - Reimplementation of Electrum-server
|
||||
===============================================
|
||||
::
|
||||
|
||||
Licence: MIT Licence
|
||||
Author: Neil Booth
|
||||
Language: Python (>=3.5)
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
For privacy and other reasons, I have long wanted to run my own
|
||||
Electrum server, but for reasons I cannot remember I struggled to set
|
||||
it up or get it to work on my DragonFlyBSD system, and I lost interest
|
||||
for over a year.
|
||||
|
||||
More recently I heard that Electrum server databases were around 35GB
|
||||
in size when gzipped, and had sync times from Genesis of over a week
|
||||
(and sufficiently painful that no one seems to have done one for a
|
||||
long time) and got curious about improvements. After taking a look at
|
||||
the existing server code I decided to try a different approach.
|
||||
|
||||
I prefer Python3 over Python2, and the fact that Electrum is stuck on
|
||||
Python2 has been frustrating for a while. It's easier to change the
|
||||
server to Python3 than the client.
|
||||
|
||||
It also seemed like a good way to learn about asyncio, which is a
|
||||
wonderful and powerful feature of Python from 3.4 onwards.
|
||||
Incidentally asyncio would also make a much better way to implement
|
||||
the Electrum client.
|
||||
|
||||
Finally though no fan of most altcoins I wanted to write a codebase
|
||||
that could easily be reused for those alts that are reasonably
|
||||
compatible with Bitcoin. Such an abstraction is also useful for
|
||||
testnets, of course.
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
ElectrumX does not currently do any pruning. With luck it may never
|
||||
become necessary. So how does it achieve a much more compact database
|
||||
than Electrum server, which throws away a lot of information? And
|
||||
sync faster to boot?
|
||||
|
||||
All of the following likely play a part:
|
||||
|
||||
- more compact representation of UTXOs, the mp address index, and
|
||||
history. Electrum server stores full transaction hash and height
|
||||
for all UTXOs. In its pruned history it does the same. ElectrumX
|
||||
just stores the transaction number in the linear history of
|
||||
transactions, and it looks like that for at least 5 years that will
|
||||
fit in a 4-byte integer. ElectrumX calculates the height from a
|
||||
simple lookup in a linear array which is stored on disk. ElectrumX
|
||||
also stores transaction hashes in a linear array on disk.
|
||||
- storing static append-only metadata which is indexed by position on
|
||||
disk rather than in levelDB. It would be nice to do this for histories
|
||||
but I cannot think how they could be easily indexable on a filesystem.
|
||||
- avoiding unnecessary or redundant computations
|
||||
- more efficient memory usage - through more compact data structures and
|
||||
and judicious use of memoryviews
|
||||
- big caches (controlled via FLUSH_SIZE)
|
||||
- asyncio and asynchronous prefetch of blocks. With luck ElectrumX
|
||||
will have no need of threads or locking primitives
|
||||
- because it prunes electrum-server needs to store undo information,
|
||||
ElectrumX should does not need to store undo information for
|
||||
blockchain reorganisations (note blockchain reorgs are not yet
|
||||
implemented in ElectrumX)
|
||||
- finally electrum-server maintains a patricia tree of UTXOs. My
|
||||
understanding is this is for future features and not currently
|
||||
required. It's unclear precisely how this will be used or what
|
||||
could replace or duplicate its functionality in ElectrumX. Since
|
||||
ElectrumX stores all necessary blockchain metadata some solution
|
||||
should exist.
|
||||
|
||||
|
||||
Future/TODO
|
||||
===========
|
||||
|
||||
- handling blockchain reorgs
|
||||
- handling client connections (heh!)
|
||||
- investigating leveldb space / speed tradeoffs
|
||||
- seeking out further efficiencies. ElectrumX is CPU bound; it would not
|
||||
surprise me if there is a way to cut CPU load by 10-20% more. To squeeze
|
||||
more out would probably require some things to move to C or C++.
|
||||
|
||||
Once I get round to writing the server part, I will add DoS
|
||||
protections if necessary to defend against requests for large
|
||||
histories. However with asyncio it would not surprise me if ElectrumX
|
||||
could smoothly serve the whole history of the biggest Satoshi dice
|
||||
address with minimal negative impact on other connections; we shall
|
||||
have to see. If the requestor is running Electrum client I am
|
||||
confident that it would collapse under the load far more quickly that
|
||||
the server would; it is very inefficeint at handling large wallets
|
||||
and histories.
|
||||
|
||||
|
||||
Database Format
|
||||
===============
|
||||
|
||||
The database and metadata formats of ElectrumX are very likely to
|
||||
change in the future. If so old DBs would not be usable. However it
|
||||
should be easy to write short Python script to do any necessary
|
||||
conversions in-place without having to start afresh.
|
||||
|
||||
|
||||
Miscellany
|
||||
==========
|
||||
|
||||
As I've been researching where the time is going during block chain
|
||||
indexing and how various cache sizes and hardware choices affect it,
|
||||
I'd appreciate it if anyone trying to synchronize could tell me their::
|
||||
|
||||
- their O/S and filesystem
|
||||
- their hardware (CPU name and speed, RAM, and disk kind)
|
||||
- whether their daemon was on the same host or not
|
||||
- whatever stats about sync height vs time they can provide (the
|
||||
logs give it all in wall time)
|
||||
- the network they synced
|
||||
|
||||
|
||||
Neil Booth
|
||||
kyuupichan@gmail.com
|
||||
https://github.com/kyuupichan
|
||||
1BWwXJH3q6PRsizBkSGm2Uw4Sz1urZ5sCj
|
||||
240
lib/coins.py
Normal file
240
lib/coins.py
Normal file
@ -0,0 +1,240 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from lib.hash import Base58, hash160
|
||||
from lib.script import ScriptPubKey
|
||||
from lib.tx import Deserializer
|
||||
|
||||
|
||||
class CoinError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Coin(object):
|
||||
'''Base class of coin hierarchy'''
|
||||
|
||||
# Not sure if these are coin-specific
|
||||
HEADER_LEN = 80
|
||||
DEFAULT_RPC_PORT = 8332
|
||||
|
||||
@staticmethod
|
||||
def coins():
|
||||
is_coin = lambda obj: (inspect.isclass(obj)
|
||||
and issubclass(obj, Coin)
|
||||
and obj != Coin)
|
||||
pairs = inspect.getmembers(sys.modules[__name__], is_coin)
|
||||
# Returned in the order they appear in this file
|
||||
return [pair[1] for pair in pairs]
|
||||
|
||||
@classmethod
|
||||
def lookup_coin_class(cls, name, net):
|
||||
for coin in cls.coins():
|
||||
if (coin.NAME.lower() == name.lower()
|
||||
and coin.NET.lower() == net.lower()):
|
||||
return coin
|
||||
raise CoinError('unknown coin {} and network {} combination'
|
||||
.format(name, net))
|
||||
|
||||
@staticmethod
|
||||
def lookup_xverbytes(verbytes):
|
||||
# Order means BTC testnet will override NMC testnet
|
||||
for coin in Coin.coins():
|
||||
if verbytes == coin.XPUB_VERBYTES:
|
||||
return True, coin
|
||||
if verbytes == coin.XPRV_VERBYTES:
|
||||
return False, coin
|
||||
raise CoinError("version bytes unrecognised")
|
||||
|
||||
@classmethod
|
||||
def address_to_hash160(cls, addr):
|
||||
'''Returns a hash160 given an address'''
|
||||
result = Base58.decode_check(addr)
|
||||
if len(result) != 21:
|
||||
raise CoinError('invalid address: {}'.format(addr))
|
||||
return result[1:]
|
||||
|
||||
@classmethod
|
||||
def P2PKH_address_from_hash160(cls, hash_bytes):
|
||||
'''Returns a P2PKH address given a public key'''
|
||||
assert len(hash_bytes) == 20
|
||||
payload = bytes([cls.P2PKH_VERBYTE]) + hash_bytes
|
||||
return Base58.encode_check(payload)
|
||||
|
||||
@classmethod
|
||||
def P2PKH_address_from_pubkey(cls, pubkey):
|
||||
'''Returns a coin address given a public key'''
|
||||
return cls.P2PKH_address_from_hash160(hash160(pubkey))
|
||||
|
||||
@classmethod
|
||||
def P2SH_address_from_hash160(cls, pubkey_bytes):
|
||||
'''Returns a coin address given a public key'''
|
||||
assert len(hash_bytes) == 20
|
||||
payload = bytes([cls.P2SH_VERBYTE]) + hash_bytes
|
||||
return Base58.encode_check(payload)
|
||||
|
||||
@classmethod
|
||||
def multisig_address(cls, m, pubkeys):
|
||||
'''Returns the P2SH address for an M of N multisig transaction. Pass
|
||||
the N pubkeys of which M are needed to sign it. If generating
|
||||
an address for a wallet, it is the caller's responsibility to
|
||||
sort them to ensure order does not matter for, e.g., wallet
|
||||
recovery.'''
|
||||
script = cls.pay_to_multisig_script(m, pubkeys)
|
||||
payload = bytes([cls.P2SH_VERBYTE]) + hash160(pubkey_bytes)
|
||||
return Base58.encode_check(payload)
|
||||
|
||||
@classmethod
|
||||
def pay_to_multisig_script(cls, m, pubkeys):
|
||||
'''Returns a P2SH multisig script for an M of N multisig
|
||||
transaction.'''
|
||||
return ScriptPubKey.multisig_script(m, pubkeys)
|
||||
|
||||
@classmethod
|
||||
def pay_to_pubkey_script(cls, pubkey):
|
||||
'''Returns a pubkey script that pays to pubkey. The input is the
|
||||
raw pubkey bytes (length 33 or 65).'''
|
||||
return ScriptPubKey.P2PK_script(pubkey)
|
||||
|
||||
@classmethod
|
||||
def pay_to_address_script(cls, address):
|
||||
'''Returns a pubkey script that pays to pubkey hash. Input is the
|
||||
address (either P2PKH or P2SH) in base58 form.'''
|
||||
raw = Base58.decode_check(address)
|
||||
|
||||
# Require version byte plus hash160.
|
||||
verbyte = -1
|
||||
if len(raw) == 21:
|
||||
verbyte, hash_bytes = raw[0], raw[1:]
|
||||
|
||||
if verbyte == cls.P2PKH_VERYBYTE:
|
||||
return ScriptPubKey.P2PKH_script(hash_bytes)
|
||||
if verbyte == cls.P2SH_VERBYTE:
|
||||
return ScriptPubKey.P2SH_script(hash_bytes)
|
||||
|
||||
raise CoinError('invalid address: {}'.format(address))
|
||||
|
||||
@classmethod
|
||||
def prvkey_WIF(privkey_bytes, compressed):
|
||||
"The private key encoded in Wallet Import Format"
|
||||
payload = bytearray([cls.WIF_BYTE]) + privkey_bytes
|
||||
if compressed:
|
||||
payload.append(0x01)
|
||||
return Base58.encode_check(payload)
|
||||
|
||||
@classmethod
|
||||
def read_block(cls, block):
|
||||
assert isinstance(block, memoryview)
|
||||
d = Deserializer(block[cls.HEADER_LEN:])
|
||||
return d.read_block()
|
||||
|
||||
|
||||
class Bitcoin(Coin):
|
||||
NAME = "Bitcoin"
|
||||
SHORTNAME = "BTC"
|
||||
NET = "mainnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("0488b21e")
|
||||
XPRV_VERBYTES = bytes.fromhex("0488ade4")
|
||||
P2PKH_VERBYTE = 0x00
|
||||
P2SH_VERBYTE = 0x05
|
||||
WIF_BYTE = 0x80
|
||||
GENESIS_HASH=(b'000000000019d6689c085ae165831e93'
|
||||
b'4ff763ae46a2a6c172b3f1b60a8ce26f')
|
||||
|
||||
class BitcoinTestnet(Coin):
|
||||
NAME = "Bitcoin"
|
||||
SHORTNAME = "XTN"
|
||||
NET = "testnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("043587cf")
|
||||
XPRV_VERBYTES = bytes.fromhex("04358394")
|
||||
P2PKH_VERBYTE = 0x6f
|
||||
P2SH_VERBYTE = 0xc4
|
||||
WIF_BYTE = 0xef
|
||||
|
||||
# Source: pycoin and others
|
||||
class Litecoin(Coin):
|
||||
NAME = "Litecoin"
|
||||
SHORTNAME = "LTC"
|
||||
NET = "mainnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("019da462")
|
||||
XPRV_VERBYTES = bytes.fromhex("019d9cfe")
|
||||
P2PKH_VERBYTE = 0x30
|
||||
P2SH_VERBYTE = 0x05
|
||||
WIF_BYTE = 0xb0
|
||||
|
||||
class LitecoinTestnet(Coin):
|
||||
NAME = "Litecoin"
|
||||
SHORTNAME = "XLT"
|
||||
NET = "testnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("0436f6e1")
|
||||
XPRV_VERBYTES = bytes.fromhex("0436ef7d")
|
||||
P2PKH_VERBYTE = 0x6f
|
||||
P2SH_VERBYTE = 0xc4
|
||||
WIF_BYTE = 0xef
|
||||
|
||||
# Source: namecoin.org
|
||||
class Namecoin(Coin):
|
||||
NAME = "Namecoin"
|
||||
SHORTNAME = "NMC"
|
||||
NET = "mainnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("d7dd6370")
|
||||
XPRV_VERBYTES = bytes.fromhex("d7dc6e31")
|
||||
P2PKH_VERBYTE = 0x34
|
||||
P2SH_VERBYTE = 0x0d
|
||||
WIF_BYTE = 0xe4
|
||||
|
||||
class NamecoinTestnet(Coin):
|
||||
NAME = "Namecoin"
|
||||
SHORTNAME = "XNM"
|
||||
NET = "testnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("043587cf")
|
||||
XPRV_VERBYTES = bytes.fromhex("04358394")
|
||||
P2PKH_VERBYTE = 0x6f
|
||||
P2SH_VERBYTE = 0xc4
|
||||
WIF_BYTE = 0xef
|
||||
|
||||
# For DOGE there is disagreement across sites like bip32.org and
|
||||
# pycoin. Taken from bip32.org and bitmerchant on github
|
||||
class Dogecoin(Coin):
|
||||
NAME = "Dogecoin"
|
||||
SHORTNAME = "DOGE"
|
||||
NET = "mainnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("02facafd")
|
||||
XPRV_VERBYTES = bytes.fromhex("02fac398")
|
||||
P2PKH_VERBYTE = 0x1e
|
||||
P2SH_VERBYTE = 0x16
|
||||
WIF_BYTE = 0x9e
|
||||
|
||||
class DogecoinTestnet(Coin):
|
||||
NAME = "Dogecoin"
|
||||
SHORTNAME = "XDT"
|
||||
NET = "testnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("0432a9a8")
|
||||
XPRV_VERBYTES = bytes.fromhex("0432a243")
|
||||
P2PKH_VERBYTE = 0x71
|
||||
P2SH_VERBYTE = 0xc4
|
||||
WIF_BYTE = 0xf1
|
||||
|
||||
# Source: pycoin
|
||||
class Dash(Coin):
|
||||
NAME = "Dash"
|
||||
SHORTNAME = "DASH"
|
||||
NET = "mainnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("02fe52cc")
|
||||
XPRV_VERBYTES = bytes.fromhex("02fe52f8")
|
||||
P2PKH_VERBYTE = 0x4c
|
||||
P2SH_VERBYTE = 0x10
|
||||
WIF_BYTE = 0xcc
|
||||
|
||||
class DashTestnet(Coin):
|
||||
NAME = "Dogecoin"
|
||||
SHORTNAME = "tDASH"
|
||||
NET = "testnet"
|
||||
XPUB_VERBYTES = bytes.fromhex("3a805837")
|
||||
XPRV_VERBYTES = bytes.fromhex("3a8061a0")
|
||||
P2PKH_VERBYTE = 0x8b
|
||||
P2SH_VERBYTE = 0x13
|
||||
WIF_BYTE = 0xef
|
||||
45
lib/enum.py
Normal file
45
lib/enum.py
Normal file
@ -0,0 +1,45 @@
|
||||
# enum-like type
|
||||
# From the Python Cookbook from http://code.activestate.com/recipes/67107/
|
||||
|
||||
|
||||
class EnumException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Enumeration:
|
||||
|
||||
def __init__(self, name, enumList):
|
||||
self.__doc__ = name
|
||||
|
||||
lookup = {}
|
||||
reverseLookup = {}
|
||||
i = 0
|
||||
uniqueNames = set()
|
||||
uniqueValues = set()
|
||||
for x in enumList:
|
||||
if isinstance(x, tuple):
|
||||
x, i = x
|
||||
if not isinstance(x, str):
|
||||
raise EnumException("enum name {} not a string".format(x))
|
||||
if not isinstance(i, int):
|
||||
raise EnumException("enum value {} not an integer".format(i))
|
||||
if x in uniqueNames:
|
||||
raise EnumException("enum name {} not unique".format(x))
|
||||
if i in uniqueValues:
|
||||
raise EnumException("enum value {} not unique".format(x))
|
||||
uniqueNames.add(x)
|
||||
uniqueValues.add(i)
|
||||
lookup[x] = i
|
||||
reverseLookup[i] = x
|
||||
i = i + 1
|
||||
self.lookup = lookup
|
||||
self.reverseLookup = reverseLookup
|
||||
|
||||
def __getattr__(self, attr):
|
||||
result = self.lookup.get(attr)
|
||||
if result is None:
|
||||
raise AttributeError('enumeration has no member {}'.format(attr))
|
||||
return result
|
||||
|
||||
def whatis(self, value):
|
||||
return self.reverseLookup[value]
|
||||
115
lib/hash.py
Normal file
115
lib/hash.py
Normal file
@ -0,0 +1,115 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from lib.util import bytes_to_int, int_to_bytes
|
||||
|
||||
|
||||
def sha256(x):
|
||||
assert isinstance(x, (bytes, bytearray, memoryview))
|
||||
return hashlib.sha256(x).digest()
|
||||
|
||||
|
||||
def ripemd160(x):
|
||||
assert isinstance(x, (bytes, bytearray, memoryview))
|
||||
h = hashlib.new('ripemd160')
|
||||
h.update(x)
|
||||
return h.digest()
|
||||
|
||||
|
||||
def double_sha256(x):
|
||||
return sha256(sha256(x))
|
||||
|
||||
|
||||
def hmac_sha512(key, msg):
|
||||
return hmac.new(key, msg, hashlib.sha512).digest()
|
||||
|
||||
|
||||
def hash160(x):
|
||||
return ripemd160(sha256(x))
|
||||
|
||||
|
||||
class InvalidBase58String(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidBase58CheckSum(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Base58(object):
|
||||
|
||||
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
assert len(chars) == 58
|
||||
cmap = {c: n for n, c in enumerate(chars)}
|
||||
|
||||
@staticmethod
|
||||
def char_value(c):
|
||||
val = Base58.cmap.get(c)
|
||||
if val is None:
|
||||
raise InvalidBase58String
|
||||
return val
|
||||
|
||||
@staticmethod
|
||||
def decode(txt):
|
||||
"""Decodes txt into a big-endian bytearray."""
|
||||
if not isinstance(txt, str):
|
||||
raise InvalidBase58String("a string is required")
|
||||
|
||||
if not txt:
|
||||
raise InvalidBase58String("string cannot be empty")
|
||||
|
||||
value = 0
|
||||
for c in txt:
|
||||
value = value * 58 + Base58.char_value(c)
|
||||
|
||||
result = int_to_bytes(value)
|
||||
|
||||
# Prepend leading zero bytes if necessary
|
||||
count = 0
|
||||
for c in txt:
|
||||
if c != '1':
|
||||
break
|
||||
count += 1
|
||||
if count:
|
||||
result = bytes(count) + result
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def encode(be_bytes):
|
||||
"""Converts a big-endian bytearray into a base58 string."""
|
||||
value = bytes_to_int(be_bytes)
|
||||
|
||||
txt = ''
|
||||
while value:
|
||||
value, mod = divmod(value, 58)
|
||||
txt += Base58.chars[mod]
|
||||
|
||||
for byte in be_bytes:
|
||||
if byte != 0:
|
||||
break
|
||||
txt += '1'
|
||||
|
||||
return txt[::-1]
|
||||
|
||||
@staticmethod
|
||||
def decode_check(txt):
|
||||
'''Decodes a Base58Check-encoded string to a payload. The version
|
||||
prefixes it.'''
|
||||
be_bytes = Base58.decode(txt)
|
||||
result, check = be_bytes[:-4], be_bytes[-4:]
|
||||
if check != double_sha256(result)[:4]:
|
||||
raise InvalidBase58CheckSum
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def encode_check(payload):
|
||||
"""Encodes a payload bytearray (which includes the version byte(s))
|
||||
into a Base58Check string."""
|
||||
assert isinstance(payload, (bytes, bytearray))
|
||||
|
||||
be_bytes = payload + double_sha256(payload)[:4]
|
||||
return Base58.encode(be_bytes)
|
||||
306
lib/script.py
Normal file
306
lib/script.py
Normal file
@ -0,0 +1,306 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
from binascii import hexlify
|
||||
import struct
|
||||
|
||||
from lib.enum import Enumeration
|
||||
from lib.hash import hash160
|
||||
from lib.util import cachedproperty
|
||||
|
||||
|
||||
class ScriptError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
OpCodes = Enumeration("Opcodes", [
|
||||
("OP_0", 0), ("OP_PUSHDATA1", 76),
|
||||
"OP_PUSHDATA2", "OP_PUSHDATA4", "OP_1NEGATE",
|
||||
"OP_RESERVED",
|
||||
"OP_1", "OP_2", "OP_3", "OP_4", "OP_5", "OP_6", "OP_7", "OP_8",
|
||||
"OP_9", "OP_10", "OP_11", "OP_12", "OP_13", "OP_14", "OP_15", "OP_16",
|
||||
"OP_NOP", "OP_VER", "OP_IF", "OP_NOTIF", "OP_VERIF", "OP_VERNOTIF",
|
||||
"OP_ELSE", "OP_ENDIF", "OP_VERIFY", "OP_RETURN",
|
||||
"OP_TOALTSTACK", "OP_FROMALTSTACK", "OP_2DROP", "OP_2DUP", "OP_3DUP",
|
||||
"OP_2OVER", "OP_2ROT", "OP_2SWAP", "OP_IFDUP", "OP_DEPTH", "OP_DROP",
|
||||
"OP_DUP", "OP_NIP", "OP_OVER", "OP_PICK", "OP_ROLL", "OP_ROT",
|
||||
"OP_SWAP", "OP_TUCK",
|
||||
"OP_CAT", "OP_SUBSTR", "OP_LEFT", "OP_RIGHT", "OP_SIZE",
|
||||
"OP_INVERT", "OP_AND", "OP_OR", "OP_XOR", "OP_EQUAL", "OP_EQUALVERIFY",
|
||||
"OP_RESERVED1", "OP_RESERVED2",
|
||||
"OP_1ADD", "OP_1SUB", "OP_2MUL", "OP_2DIV", "OP_NEGATE", "OP_ABS",
|
||||
"OP_NOT", "OP_0NOTEQUAL", "OP_ADD", "OP_SUB", "OP_MUL", "OP_DIV", "OP_MOD",
|
||||
"OP_LSHIFT", "OP_RSHIFT", "OP_BOOLAND", "OP_BOOLOR", "OP_NUMEQUAL",
|
||||
"OP_NUMEQUALVERIFY", "OP_NUMNOTEQUAL", "OP_LESSTHAN", "OP_GREATERTHAN",
|
||||
"OP_LESSTHANOREQUAL", "OP_GREATERTHANOREQUAL", "OP_MIN", "OP_MAX",
|
||||
"OP_WITHIN",
|
||||
"OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160", "OP_HASH256",
|
||||
"OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
|
||||
"OP_CHECKMULTISIGVERIFY",
|
||||
"OP_NOP1",
|
||||
"OP_CHECKLOCKTIMEVERIFY", "OP_CHECKSEQUENCEVERIFY"
|
||||
])
|
||||
|
||||
|
||||
# Paranoia to make it hard to create bad scripts
|
||||
assert OpCodes.OP_DUP == 0x76
|
||||
assert OpCodes.OP_HASH160 == 0xa9
|
||||
assert OpCodes.OP_EQUAL == 0x87
|
||||
assert OpCodes.OP_EQUALVERIFY == 0x88
|
||||
assert OpCodes.OP_CHECKSIG == 0xac
|
||||
assert OpCodes.OP_CHECKMULTISIG == 0xae
|
||||
|
||||
|
||||
class ScriptSig(object):
|
||||
'''A script from a tx input, typically provides one or more signatures.'''
|
||||
|
||||
SIG_ADDRESS, SIG_MULTI, SIG_PUBKEY, SIG_UNKNOWN = range(4)
|
||||
|
||||
def __init__(self, script, coin, kind, sigs, pubkeys):
|
||||
self.script = script
|
||||
self.coin = coin
|
||||
self.kind = kind
|
||||
self.sigs = sigs
|
||||
self.pubkeys = pubkeys
|
||||
|
||||
@cachedproperty
|
||||
def address(self):
|
||||
if self.kind == SIG_ADDRESS:
|
||||
return self.coin.address_from_pubkey(self.pubkeys[0])
|
||||
if self.kind == SIG_MULTI:
|
||||
return self.coin.multsig_address(self.pubkeys)
|
||||
return 'Unknown'
|
||||
|
||||
@classmethod
|
||||
def from_script(cls, script, coin):
|
||||
'''Returns an instance of this class. Uncrecognised scripts return
|
||||
an object of kind SIG_UNKNOWN.'''
|
||||
try:
|
||||
return cls.parse_script(script, coin)
|
||||
except ScriptError:
|
||||
return cls(script, coin, SIG_UNKNOWN, [], [])
|
||||
|
||||
@classmethod
|
||||
def parse_script(cls, script, coin):
|
||||
'''Returns an instance of this class. Raises on unrecognised
|
||||
scripts.'''
|
||||
ops, datas = Script.get_ops(script)
|
||||
|
||||
# Address, PubKey and P2SH redeems only push data
|
||||
if not ops or not Script.match_ops(ops, [-1] * len(ops)):
|
||||
raise ScriptError('unknown scriptsig pattern')
|
||||
|
||||
# Assume double data pushes are address redeems, single data
|
||||
# pushes are pubkey redeems
|
||||
if len(ops) == 2: # Signature, pubkey
|
||||
return cls(script, coin, SIG_ADDRESS, [datas[0]], [datas[1]])
|
||||
|
||||
if len(ops) == 1: # Pubkey
|
||||
return cls(script, coin, SIG_PUBKEY, [datas[0]], [])
|
||||
|
||||
# Presumably it is P2SH (though conceivably the above could be
|
||||
# too; cannot be sure without the send-to script). We only
|
||||
# handle CHECKMULTISIG P2SH, which because of a bitcoin core
|
||||
# bug always start with an unused OP_0.
|
||||
if ops[0] != OpCodes.OP_0:
|
||||
raise ScriptError('unknown scriptsig pattern; expected OP_0')
|
||||
|
||||
# OP_0, Sig1, ..., SigM, pk_script
|
||||
m = len(ops) - 2
|
||||
pk_script = datas[-1]
|
||||
pk_ops, pk_datas = Script.get_ops(script)
|
||||
|
||||
# OP_2 pubkey1 pubkey2 pubkey3 OP_3 OP_CHECKMULTISIG
|
||||
n = len(pk_ops) - 3
|
||||
pattern = ([OpCodes.OP_1 + m - 1] + [-1] * n
|
||||
+ [OpCodes.OP_1 + n - 1, OpCodes.OP_CHECKMULTISIG])
|
||||
|
||||
if m <= n and Script.match_ops(pk_ops, pattern):
|
||||
return cls(script, coin, SIG_MULTI, datas[1:-1], pk_datas[1:-2])
|
||||
|
||||
raise ScriptError('unknown multisig P2SH pattern')
|
||||
|
||||
|
||||
class ScriptPubKey(object):
|
||||
'''A script from a tx output that gives conditions necessary for
|
||||
spending.'''
|
||||
|
||||
TO_ADDRESS, TO_P2SH, TO_PUBKEY, TO_UNKNOWN = range(4)
|
||||
TO_ADDRESS_OPS = [OpCodes.OP_DUP, OpCodes.OP_HASH160, -1,
|
||||
OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG]
|
||||
TO_P2SH_OPS = [OpCodes.OP_HASH160, -1, OpCodes.OP_EQUAL]
|
||||
TO_PUBKEY_OPS = [-1, OpCodes.OP_CHECKSIG]
|
||||
|
||||
def __init__(self, script, coin, kind, hash160, pubkey=None):
|
||||
self.script = script
|
||||
self.coin = coin
|
||||
self.kind = kind
|
||||
self.hash160 = hash160
|
||||
if pubkey:
|
||||
self.pubkey = pubkey
|
||||
|
||||
@cachedproperty
|
||||
def address(self):
|
||||
if self.kind == ScriptPubKey.TO_P2SH:
|
||||
return self.coin.P2SH_address_from_hash160(self.hash160)
|
||||
if self.hash160:
|
||||
return self.coin.P2PKH_address_from_hash160(self.hash160)
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def from_script(cls, script, coin):
|
||||
'''Returns an instance of this class. Uncrecognised scripts return
|
||||
an object of kind TO_UNKNOWN.'''
|
||||
try:
|
||||
return cls.parse_script(script, coin)
|
||||
except ScriptError:
|
||||
return cls(script, coin, cls.TO_UNKNOWN, None)
|
||||
|
||||
@classmethod
|
||||
def parse_script(cls, script, coin):
|
||||
'''Returns an instance of this class. Raises on unrecognised
|
||||
scripts.'''
|
||||
ops, datas = Script.get_ops(script)
|
||||
|
||||
if Script.match_ops(ops, cls.TO_ADDRESS_OPS):
|
||||
return cls(script, coin, cls.TO_ADDRESS, datas[2])
|
||||
|
||||
if Script.match_ops(ops, cls.TO_P2SH_OPS):
|
||||
return cls(script, coin, cls.TO_P2SH, datas[1])
|
||||
|
||||
if Script.match_ops(ops, cls.TO_PUBKEY_OPS):
|
||||
pubkey = datas[0]
|
||||
return cls(script, coin, cls.TO_PUBKEY, hash160(pubkey), pubkey)
|
||||
|
||||
raise ScriptError('unknown script pubkey pattern')
|
||||
|
||||
@classmethod
|
||||
def P2SH_script(cls, hash160):
|
||||
return (bytes([OpCodes.OP_HASH160])
|
||||
+ Script.push_data(hash160)
|
||||
+ bytes([OpCodes.OP_EQUAL]))
|
||||
|
||||
@classmethod
|
||||
def P2PKH_script(cls, hash160):
|
||||
return (bytes([OpCodes.OP_DUP, OpCodes.OP_HASH160])
|
||||
+ Script.push_data(hash160)
|
||||
+ bytes([OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG]))
|
||||
|
||||
@classmethod
|
||||
def validate_pubkey(cls, pubkey, req_compressed=False):
|
||||
if isinstance(pubkey, (bytes, bytearray)):
|
||||
if len(pubkey) == 33 and pubkey[0] in (2, 3):
|
||||
return # Compressed
|
||||
if len(pubkey) == 65 and pubkey[0] == 4:
|
||||
if not req_compressed:
|
||||
return
|
||||
raise PubKeyError('uncompressed pubkeys are invalid')
|
||||
raise PubKeyError('invalid pubkey {}'.format(pubkey))
|
||||
|
||||
@classmethod
|
||||
def pubkey_script(cls, pubkey):
|
||||
cls.validate_pubkey(pubkey)
|
||||
return Script.push_data(pubkey) + bytes([OpCodes.OP_CHECKSIG])
|
||||
|
||||
@classmethod
|
||||
def multisig_script(cls, m, pubkeys):
|
||||
'''Returns the script for a pay-to-multisig transaction.'''
|
||||
n = len(pubkeys)
|
||||
if not 1 <= m <= n <= 15:
|
||||
raise ScriptError('{:d} of {:d} multisig script not possible'
|
||||
.format(m, n))
|
||||
for pubkey in pubkeys:
|
||||
cls.validate_pubkey(pubkey, req_compressed=True)
|
||||
# See https://bitcoin.org/en/developer-guide
|
||||
# 2 of 3 is: OP_2 pubkey1 pubkey2 pubkey3 OP_3 OP_CHECKMULTISIG
|
||||
return (bytes([OP_1 + m - 1])
|
||||
+ b''.join(cls.push_data(pubkey) for pubkey in pubkeys)
|
||||
+ bytes([OP_1 + n - 1, OP_CHECK_MULTISIG]))
|
||||
|
||||
|
||||
class Script(object):
|
||||
|
||||
@classmethod
|
||||
def get_ops(cls, script):
|
||||
opcodes, datas = [], []
|
||||
|
||||
# The unpacks or script[n] below throw on truncated scripts
|
||||
try:
|
||||
n = 0
|
||||
while n < len(script):
|
||||
opcode, data = script[n], None
|
||||
n += 1
|
||||
|
||||
if opcode <= OpCodes.OP_PUSHDATA4:
|
||||
# Raw bytes follow
|
||||
if opcode < OpCodes.OP_PUSHDATA1:
|
||||
dlen = opcode
|
||||
elif opcode == OpCodes.OP_PUSHDATA1:
|
||||
dlen = script[n]
|
||||
n += 1
|
||||
elif opcode == OpCodes.OP_PUSHDATA2:
|
||||
(dlen,) = struct.unpack('<H', script[n: n + 2])
|
||||
n += 2
|
||||
else:
|
||||
(dlen,) = struct.unpack('<I', script[n: n + 4])
|
||||
n += 4
|
||||
data = script[n:n + dlen]
|
||||
if len(data) != dlen:
|
||||
raise ScriptError('truncated script')
|
||||
n += dlen
|
||||
|
||||
opcodes.append(opcode)
|
||||
datas.append(data)
|
||||
except:
|
||||
# Truncated script; e.g. tx_hash
|
||||
# ebc9fa1196a59e192352d76c0f6e73167046b9d37b8302b6bb6968dfd279b767
|
||||
raise ScriptError('truncated script')
|
||||
|
||||
return opcodes, datas
|
||||
|
||||
@classmethod
|
||||
def match_ops(cls, ops, pattern):
|
||||
if len(ops) != len(pattern):
|
||||
return False
|
||||
for op, pop in zip(ops, pattern):
|
||||
if pop != op:
|
||||
# -1 Indicates data push expected
|
||||
if pop == -1 and OpCodes.OP_0 <= op <= OpCodes.OP_PUSHDATA4:
|
||||
continue
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def push_data(cls, data):
|
||||
'''Returns the opcodes to push the data on the stack.'''
|
||||
assert isinstance(data, (bytes, bytearray))
|
||||
|
||||
n = len(data)
|
||||
if n < OpCodes.OP_PUSHDATA1:
|
||||
return bytes([n]) + data
|
||||
if n < 256:
|
||||
return bytes([OpCodes.OP_PUSHDATA1, n]) + data
|
||||
if n < 65536:
|
||||
return bytes([OpCodes.OP_PUSHDATA2]) + struct.pack('<H', n) + data
|
||||
return bytes([OpCodes.OP_PUSHDATA4]) + struct.pack('<I', n) + data
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
if OpCodes.OP_0 < opcode < OpCodes.OP_PUSHDATA1:
|
||||
return 'OP_{:d}'.format(opcode)
|
||||
try:
|
||||
return OpCodes.whatis(opcode)
|
||||
except KeyError:
|
||||
return 'OP_UNKNOWN:{:d}'.format(opcode)
|
||||
|
||||
@classmethod
|
||||
def dump(cls, script):
|
||||
opcodes, datas = cls.get_ops(script)
|
||||
for opcode, data in zip(opcodes, datas):
|
||||
name = cls.opcode_name(opcode)
|
||||
if data is None:
|
||||
print(name)
|
||||
else:
|
||||
print('{} {} ({:d} bytes)'
|
||||
.format(name, hexlify(data).decode('ascii'), len(data)))
|
||||
140
lib/tx.py
Normal file
140
lib/tx.py
Normal file
@ -0,0 +1,140 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
from collections import namedtuple
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
from lib.util import cachedproperty
|
||||
from lib.hash import double_sha256
|
||||
|
||||
|
||||
class Tx(namedtuple("Tx", "version inputs outputs locktime")):
|
||||
|
||||
@cachedproperty
|
||||
def is_coinbase(self):
|
||||
return self.inputs[0].is_coinbase
|
||||
|
||||
OutPoint = namedtuple("OutPoint", "hash n")
|
||||
|
||||
# prevout is an OutPoint object
|
||||
class TxInput(namedtuple("TxInput", "prevout script sequence")):
|
||||
|
||||
ZERO = bytes(32)
|
||||
MINUS_1 = 4294967295
|
||||
|
||||
@cachedproperty
|
||||
def is_coinbase(self):
|
||||
return self.prevout == (TxInput.ZERO, TxInput.MINUS_1)
|
||||
|
||||
@cachedproperty
|
||||
def script_sig_info(self):
|
||||
# No meaning for coinbases
|
||||
if self.is_coinbase:
|
||||
return None
|
||||
return Script.parse_script_sig(self.script)
|
||||
|
||||
def __repr__(self):
|
||||
script = binascii.hexlify(self.script).decode("ascii")
|
||||
prev_hash = binascii.hexlify(self.prevout.hash).decode("ascii")
|
||||
return ("Input(prevout=({}, {:d}), script={}, sequence={:d})"
|
||||
.format(prev_hash, self.prevout.n, script, self.sequence))
|
||||
|
||||
|
||||
class TxOutput(namedtuple("TxOutput", "value pk_script")):
|
||||
|
||||
@cachedproperty
|
||||
def pay_to(self):
|
||||
return Script.parse_pk_script(self.pk_script)
|
||||
|
||||
|
||||
class Deserializer(object):
|
||||
|
||||
def __init__(self, binary):
|
||||
assert isinstance(binary, (bytes, memoryview))
|
||||
self.binary = binary
|
||||
self.cursor = 0
|
||||
|
||||
def read_tx(self):
|
||||
version = self.read_le_int32()
|
||||
inputs = self.read_inputs()
|
||||
outputs = self.read_outputs()
|
||||
locktime = self.read_le_uint32()
|
||||
return Tx(version, inputs, outputs, locktime)
|
||||
|
||||
def read_block(self):
|
||||
tx_hashes = []
|
||||
txs = []
|
||||
tx_count = self.read_varint()
|
||||
for n in range(tx_count):
|
||||
start = self.cursor
|
||||
tx = self.read_tx()
|
||||
# Note this hash needs to be reversed for human display
|
||||
# For efficiency we store it in the natural serialized order
|
||||
tx_hash = double_sha256(self.binary[start:self.cursor])
|
||||
tx_hashes.append(tx_hash)
|
||||
txs.append(tx)
|
||||
return tx_hashes, txs
|
||||
|
||||
def read_inputs(self):
|
||||
n = self.read_varint()
|
||||
return [self.read_input() for i in range(n)]
|
||||
|
||||
def read_input(self):
|
||||
prevout = self.read_outpoint()
|
||||
script = self.read_varbytes()
|
||||
sequence = self.read_le_uint32()
|
||||
return TxInput(prevout, script, sequence)
|
||||
|
||||
def read_outpoint(self):
|
||||
hash = self.read_nbytes(32)
|
||||
n = self.read_le_uint32()
|
||||
return OutPoint(hash, n)
|
||||
|
||||
def read_outputs(self):
|
||||
n = self.read_varint()
|
||||
return [self.read_output() for i in range(n)]
|
||||
|
||||
def read_output(self):
|
||||
value = self.read_le_int64()
|
||||
pk_script = self.read_varbytes()
|
||||
return TxOutput(value, pk_script)
|
||||
|
||||
def read_nbytes(self, n):
|
||||
result = self.binary[self.cursor:self.cursor + n]
|
||||
self.cursor += n
|
||||
return result
|
||||
|
||||
def read_varbytes(self):
|
||||
return self.read_nbytes(self.read_varint())
|
||||
|
||||
def read_varint(self):
|
||||
b = self.binary[self.cursor]
|
||||
self.cursor += 1
|
||||
if b < 253:
|
||||
return b
|
||||
if b == 253:
|
||||
return self.read_le_uint16()
|
||||
if b == 254:
|
||||
return self.read_le_uint32()
|
||||
return self.read_le_uint64()
|
||||
|
||||
def read_le_int32(self):
|
||||
return self.read_format('<i')
|
||||
|
||||
def read_le_int64(self):
|
||||
return self.read_format('<q')
|
||||
|
||||
def read_le_uint16(self):
|
||||
return self.read_format('<H')
|
||||
|
||||
def read_le_uint32(self):
|
||||
return self.read_format('<I')
|
||||
|
||||
def read_le_uint64(self):
|
||||
return self.read_format('<Q')
|
||||
|
||||
def read_format(self, fmt):
|
||||
(result,) = struct.unpack_from(fmt, self.binary, self.cursor)
|
||||
self.cursor += struct.calcsize(fmt)
|
||||
return result
|
||||
66
lib/util.py
Normal file
66
lib/util.py
Normal file
@ -0,0 +1,66 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class Log(object):
|
||||
'''Logging base class'''
|
||||
|
||||
VERBOSE = True
|
||||
|
||||
def diagnostic_name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def log(self, *msgs):
|
||||
if Log.VERBOSE:
|
||||
print('[{}]: '.format(self.diagnostic_name()), *msgs,
|
||||
file=sys.stdout, flush=True)
|
||||
|
||||
def log_error(self, *msg):
|
||||
print('[{}]: ERROR: {}'.format(self.diagnostic_name()), *msgs,
|
||||
file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
# Method decorator. To be used for calculations that will always
|
||||
# deliver the same result. The method cannot take any arguments
|
||||
# and should be accessed as an attribute.
|
||||
class cachedproperty(object):
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __get__(self, obj, type):
|
||||
if obj is None:
|
||||
return self
|
||||
value = self.f(obj)
|
||||
obj.__dict__[self.f.__name__] = value
|
||||
return value
|
||||
|
||||
def __set__(self, obj, value):
|
||||
raise AttributeError('cannot set {} on {}'
|
||||
.format(self.f.__name__, obj))
|
||||
|
||||
|
||||
def chunks(items, size):
|
||||
for i in range(0, len(items), size):
|
||||
yield items[i: i + size]
|
||||
|
||||
|
||||
def bytes_to_int(be_bytes):
|
||||
'''Interprets a big-endian sequence of bytes as an integer'''
|
||||
assert isinstance(be_bytes, (bytes, bytearray))
|
||||
value = 0
|
||||
for byte in be_bytes:
|
||||
value = value * 256 + byte
|
||||
return value
|
||||
|
||||
|
||||
def int_to_bytes(value):
|
||||
'''Converts an integer to a big-endian sequence of bytes'''
|
||||
mods = []
|
||||
while value:
|
||||
value, mod = divmod(value, 256)
|
||||
mods.append(mod)
|
||||
return bytes(reversed(mods))
|
||||
15
samples/scripts/NOTES
Normal file
15
samples/scripts/NOTES
Normal file
@ -0,0 +1,15 @@
|
||||
The following environment variables are required:
|
||||
|
||||
COIN - see lib/coins.py, must be a coin NAME
|
||||
NETWORK - see lib/coins.py, must be a coin NET
|
||||
DB_DIRECTORY - path to the database directory (if relative, to run script)
|
||||
USERNAME - the username the server will run as
|
||||
SERVER_MAIN - path to the server_main.py script (if relative, to run script)
|
||||
|
||||
In addition either RPC_URL must be given as the full RPC URL for
|
||||
connecting to the daemon, or you must specify RPC_HOST, RPC_USER,
|
||||
RPC_PASSWORD and optionally RPC_PORT (it defaults appropriately for
|
||||
the coin and network otherwise).
|
||||
|
||||
The other environment variables are all optional and will adopt sensible defaults if not
|
||||
specified.
|
||||
1
samples/scripts/env/COIN
vendored
Normal file
1
samples/scripts/env/COIN
vendored
Normal file
@ -0,0 +1 @@
|
||||
Bitcoin
|
||||
1
samples/scripts/env/DB_DIRECTORY
vendored
Normal file
1
samples/scripts/env/DB_DIRECTORY
vendored
Normal file
@ -0,0 +1 @@
|
||||
/path/to/db/directory
|
||||
1
samples/scripts/env/FLUSH_SIZE
vendored
Normal file
1
samples/scripts/env/FLUSH_SIZE
vendored
Normal file
@ -0,0 +1 @@
|
||||
4000000
|
||||
1
samples/scripts/env/NETWORK
vendored
Normal file
1
samples/scripts/env/NETWORK
vendored
Normal file
@ -0,0 +1 @@
|
||||
mainnet
|
||||
1
samples/scripts/env/RPC_HOST
vendored
Normal file
1
samples/scripts/env/RPC_HOST
vendored
Normal file
@ -0,0 +1 @@
|
||||
192.168.0.1
|
||||
1
samples/scripts/env/RPC_PASSWORD
vendored
Normal file
1
samples/scripts/env/RPC_PASSWORD
vendored
Normal file
@ -0,0 +1 @@
|
||||
your_daemon's_rpc_password
|
||||
1
samples/scripts/env/RPC_PORT
vendored
Normal file
1
samples/scripts/env/RPC_PORT
vendored
Normal file
@ -0,0 +1 @@
|
||||
8332
|
||||
1
samples/scripts/env/RPC_USERNAME
vendored
Normal file
1
samples/scripts/env/RPC_USERNAME
vendored
Normal file
@ -0,0 +1 @@
|
||||
your_daemon's_rpc_username
|
||||
1
samples/scripts/env/SERVER_MAIN
vendored
Normal file
1
samples/scripts/env/SERVER_MAIN
vendored
Normal file
@ -0,0 +1 @@
|
||||
/path/to/repos/electrumx/server_main.py
|
||||
1
samples/scripts/env/USERNAME
vendored
Normal file
1
samples/scripts/env/USERNAME
vendored
Normal file
@ -0,0 +1 @@
|
||||
electrumx
|
||||
2
samples/scripts/log/run
Executable file
2
samples/scripts/log/run
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec multilog t s500000 n10 /path/to/log/dir
|
||||
3
samples/scripts/run
Executable file
3
samples/scripts/run
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "Launching ElectrumX server..."
|
||||
exec 2>&1 envdir ./env /bin/sh -c 'envuidgid $USERNAME python3 $SERVER_MAIN'
|
||||
0
server/__init__.py
Normal file
0
server/__init__.py
Normal file
470
server/db.py
Normal file
470
server/db.py
Normal file
@ -0,0 +1,470 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
import array
|
||||
import itertools
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
from bisect import bisect_right
|
||||
from collections import defaultdict, namedtuple
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import plyvel
|
||||
|
||||
from lib.coins import Bitcoin
|
||||
from lib.script import ScriptPubKey
|
||||
|
||||
ADDR_TX_HASH_LEN=6
|
||||
UTXO_TX_HASH_LEN=4
|
||||
HIST_ENTRY_LEN=256*4 # Admits 65536 * HIST_ENTRY_LEN/4 entries
|
||||
UTXO = namedtuple("UTXO", "tx_num tx_pos tx_hash height value")
|
||||
|
||||
|
||||
def to_4_bytes(value):
|
||||
return struct.pack('<I', value)
|
||||
|
||||
def from_4_bytes(b):
|
||||
return struct.unpack('<I', b)[0]
|
||||
|
||||
|
||||
class DB(object):
|
||||
|
||||
HEIGHT_KEY = b'height'
|
||||
TIP_KEY = b'tip'
|
||||
GENESIS_KEY = b'genesis'
|
||||
TX_COUNT_KEY = b'tx_count'
|
||||
WALL_TIME_KEY = b'wall_time'
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, env):
|
||||
self.logger = logging.getLogger('DB')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
self.coin = env.coin
|
||||
self.flush_size = env.flush_size
|
||||
self.logger.info('using flush size of {:,d} entries'
|
||||
.format(self.flush_size))
|
||||
|
||||
self.tx_counts = array.array('I')
|
||||
self.tx_hash_file_size = 4*1024*1024
|
||||
# Unflushed items. Headers and tx_hashes have one entry per block
|
||||
self.headers = []
|
||||
self.tx_hashes = []
|
||||
self.history = defaultdict(list)
|
||||
self.writes_avoided = 0
|
||||
self.read_cache_hits = 0
|
||||
self.write_cache_hits = 0
|
||||
self.last_writes = 0
|
||||
self.last_time = time.time()
|
||||
|
||||
# Things put in a batch are not visible until the batch is written,
|
||||
# so use a cache.
|
||||
# Semantics: a key/value pair in this dictionary represents the
|
||||
# in-memory state of the DB. Anything in this dictionary will be
|
||||
# written at the next flush.
|
||||
self.write_cache = {}
|
||||
# Read cache: a key/value pair in this dictionary represents
|
||||
# something read from the DB; it is on-disk as of the prior
|
||||
# flush. If a key is in write_cache that value is more
|
||||
# recent. Any key in write_cache and not in read_cache has
|
||||
# never hit the disk.
|
||||
self.read_cache = {}
|
||||
|
||||
db_name = '{}-{}'.format(self.coin.NAME, self.coin.NET)
|
||||
try:
|
||||
self.db = self.open_db(db_name, False)
|
||||
except:
|
||||
self.db = self.open_db(db_name, True)
|
||||
self.headers_file = self.open_file('headers', True)
|
||||
self.txcount_file = self.open_file('txcount', True)
|
||||
self.init_db()
|
||||
self.logger.info('created new database {}'.format(db_name))
|
||||
else:
|
||||
self.logger.info('successfully opened database {}'.format(db_name))
|
||||
self.headers_file = self.open_file('headers')
|
||||
self.txcount_file = self.open_file('txcount')
|
||||
self.read_db()
|
||||
|
||||
# Note that DB_HEIGHT is the height of the next block to be written.
|
||||
# So an empty DB has a DB_HEIGHT of 0 not -1.
|
||||
self.tx_count = self.db_tx_count
|
||||
self.height = self.db_height - 1
|
||||
self.tx_counts.fromfile(self.txcount_file, self.db_height)
|
||||
if self.tx_count == 0:
|
||||
self.flush()
|
||||
|
||||
def open_db(self, db_name, create):
|
||||
return plyvel.DB(db_name, create_if_missing=create,
|
||||
error_if_exists=create,
|
||||
compression=None)
|
||||
# lru_cache_size=256*1024*1024)
|
||||
|
||||
def init_db(self):
|
||||
self.db_height = 0
|
||||
self.db_tx_count = 0
|
||||
self.wall_time = 0
|
||||
self.tip = self.coin.GENESIS_HASH
|
||||
self.put(self.GENESIS_KEY, unhexlify(self.tip))
|
||||
|
||||
def read_db(self):
|
||||
genesis_hash = hexlify(self.get(self.GENESIS_KEY))
|
||||
if genesis_hash != self.coin.GENESIS_HASH:
|
||||
raise self.Error('DB genesis hash {} does not match coin {}'
|
||||
.format(genesis_hash, self.coin.GENESIS_HASH))
|
||||
self.db_height = from_4_bytes(self.get(self.HEIGHT_KEY))
|
||||
self.db_tx_count = from_4_bytes(self.get(self.TX_COUNT_KEY))
|
||||
self.wall_time = from_4_bytes(self.get(self.WALL_TIME_KEY))
|
||||
self.tip = hexlify(self.get(self.TIP_KEY))
|
||||
self.logger.info('{}/{} height: {:,d} tx count: {:,d} sync time: {}'
|
||||
.format(self.coin.NAME, self.coin.NET,
|
||||
self.db_height - 1, self.db_tx_count,
|
||||
self.formatted_wall_time()))
|
||||
|
||||
def formatted_wall_time(self):
|
||||
wall_time = int(self.wall_time)
|
||||
return '{:d}d {:02d}h {:02d}m {:02d}s'.format(
|
||||
wall_time // 86400, (wall_time % 86400) // 3600,
|
||||
(wall_time % 3600) // 60, wall_time % 60)
|
||||
|
||||
def get(self, key):
|
||||
# Get a key from write_cache, then read_cache, then the DB
|
||||
value = self.write_cache.get(key)
|
||||
if not value:
|
||||
value = self.read_cache.get(key)
|
||||
if not value:
|
||||
value = self.db.get(key)
|
||||
self.read_cache[key] = value
|
||||
else:
|
||||
self.read_cache_hits += 1
|
||||
else:
|
||||
self.write_cache_hits += 1
|
||||
return value
|
||||
|
||||
def put(self, key, value):
|
||||
assert(bool(value))
|
||||
self.write_cache[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
# Deleting an on-disk key requires a later physical delete
|
||||
# If it's not on-disk we can just drop it entirely
|
||||
if self.read_cache.get(key) is None:
|
||||
self.writes_avoided += 1
|
||||
self.write_cache.pop(key, None)
|
||||
else:
|
||||
self.write_cache[key] = None
|
||||
|
||||
def put_state(self):
|
||||
now = time.time()
|
||||
self.wall_time += now - self.last_time
|
||||
self.last_time = now
|
||||
self.db_tx_count = self.tx_count
|
||||
self.db_height = self.height + 1
|
||||
self.put(self.HEIGHT_KEY, to_4_bytes(self.db_height))
|
||||
self.put(self.TX_COUNT_KEY, to_4_bytes(self.db_tx_count))
|
||||
self.put(self.TIP_KEY, unhexlify(self.tip))
|
||||
self.put(self.WALL_TIME_KEY, to_4_bytes(int(self.wall_time)))
|
||||
|
||||
def flush(self):
|
||||
# Write out the files to the FS before flushing to the DB. If
|
||||
# the DB transaction fails, the files being too long doesn't
|
||||
# matter. But if writing the files fails we do not want to
|
||||
# have updated the DB. This disk flush is fast.
|
||||
self.write_headers()
|
||||
self.write_tx_counts()
|
||||
self.write_tx_hashes()
|
||||
|
||||
tx_diff = self.tx_count - self.db_tx_count
|
||||
height_diff = self.height + 1 - self.db_height
|
||||
self.logger.info('flushing to levelDB {:,d} txs and {:,d} blocks '
|
||||
'to height {:,d} tx count: {:,d}'
|
||||
.format(tx_diff, height_diff, self.height,
|
||||
self.tx_count))
|
||||
|
||||
# This LevelDB flush is slow
|
||||
deletes = 0
|
||||
writes = 0
|
||||
with self.db.write_batch(transaction=True) as batch:
|
||||
# Flush the state, then the cache, then the history
|
||||
self.put_state()
|
||||
for key, value in self.write_cache.items():
|
||||
if value is None:
|
||||
batch.delete(key)
|
||||
deletes += 1
|
||||
else:
|
||||
batch.put(key, value)
|
||||
writes += 1
|
||||
|
||||
self.flush_history()
|
||||
|
||||
self.logger.info('flushed. Cache hits: {:,d}/{:,d} writes: {:,d} '
|
||||
'deletes: {:,d} elided: {:,d} sync: {}'
|
||||
.format(self.write_cache_hits,
|
||||
self.read_cache_hits, writes, deletes,
|
||||
self.writes_avoided,
|
||||
self.formatted_wall_time()))
|
||||
|
||||
# Note this preserves semantics and hopefully saves time
|
||||
self.read_cache = self.write_cache
|
||||
self.write_cache = {}
|
||||
self.writes_avoided = 0
|
||||
self.read_cache_hits = 0
|
||||
self.write_cache_hits = 0
|
||||
self.last_writes = writes
|
||||
|
||||
def flush_history(self):
|
||||
# Drop any None entry
|
||||
self.history.pop(None, None)
|
||||
|
||||
for hash160, hist in self.history.items():
|
||||
prefix = b'H' + hash160
|
||||
for key, v in self.db.iterator(reverse=True, prefix=prefix,
|
||||
fill_cache=False):
|
||||
assert len(key) == 23
|
||||
v += array.array('I', hist).tobytes()
|
||||
break
|
||||
else:
|
||||
key = prefix + bytes(2)
|
||||
v = array.array('I', hist).tobytes()
|
||||
|
||||
# db.put doesn't accept a memoryview!
|
||||
self.db.put(key, v[:HIST_ENTRY_LEN])
|
||||
if len(v) > HIST_ENTRY_LEN:
|
||||
# must be big-endian
|
||||
(idx, ) = struct.unpack('>H', key[-2:])
|
||||
for n in range(HIST_ENTRY_LEN, len(v), HIST_ENTRY_LEN):
|
||||
idx += 1
|
||||
key = prefix + struct.pack('>H', idx)
|
||||
if idx % 500 == 0:
|
||||
addr = self.coin.P2PKH_address_from_hash160(hash160)
|
||||
self.logger.info('address {} hist moving to idx {:d}'
|
||||
.format(addr, idx))
|
||||
self.db.put(key, v[n:n + HIST_ENTRY_LEN])
|
||||
|
||||
self.history = defaultdict(list)
|
||||
|
||||
def get_hash160(self, tx_hash, idx, delete=True):
|
||||
key = b'h' + tx_hash[:ADDR_TX_HASH_LEN] + struct.pack('<H', idx)
|
||||
data = self.get(key)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if len(data) == 24:
|
||||
if delete:
|
||||
self.delete(key)
|
||||
return data[:20]
|
||||
|
||||
# This should almost never happen
|
||||
assert len(data) % 24 == 0
|
||||
self.logger.info('hash160 compressed key collision {}'
|
||||
.format(key.hex()))
|
||||
for n in range(0, len(data), 24):
|
||||
(tx_num, ) = struct.unpack('<I', data[n+20:n+24])
|
||||
my_hash, height = self.get_tx_hash(tx_num)
|
||||
if my_hash == tx_hash:
|
||||
if delete:
|
||||
self.put(key, data[:n] + data[n + 24:])
|
||||
return data[n:n+20]
|
||||
else:
|
||||
raise Exception('could not resolve hash160 collision')
|
||||
|
||||
def spend_utxo(self, prevout):
|
||||
hash160 = self.get_hash160(prevout.hash, prevout.n)
|
||||
if hash160 is None:
|
||||
# This indicates a successful spend of a non-standard script
|
||||
# self.logger.info('ignoring spend of non-standard UTXO {}/{:d} '
|
||||
# 'at height {:d}'
|
||||
# .format(bytes(reversed(prevout.hash)).hex(),
|
||||
# prevout.n, self.height))
|
||||
return None
|
||||
|
||||
key = (b'u' + hash160 + prevout.hash[:UTXO_TX_HASH_LEN]
|
||||
+ struct.pack('<H', prevout.n))
|
||||
data = self.get(key)
|
||||
if len(data) == 12:
|
||||
(tx_num, ) = struct.unpack('<I', data[:4])
|
||||
self.delete(key)
|
||||
else:
|
||||
# This should almost never happen
|
||||
assert len(data) % (4 + 8) == 0
|
||||
self.logger.info('UTXO compressed key collision at height {:d}, '
|
||||
'utxo {} / {:d}'
|
||||
.format(self.height, bytes(reversed(prevout.hash))
|
||||
.hex(), prevout.n))
|
||||
for n in range(0, len(data), 12):
|
||||
(tx_num, ) = struct.unpack('<I', data[n:n+4])
|
||||
tx_hash, height = self.get_tx_hash(tx_num)
|
||||
if prevout.hash == tx_hash:
|
||||
break
|
||||
else:
|
||||
raise Exception('could not resolve UTXO key collision')
|
||||
|
||||
data = data[:n] + data[n + 12:]
|
||||
self.put(key, data)
|
||||
|
||||
return hash160
|
||||
|
||||
def put_utxo(self, tx_hash, idx, txout):
|
||||
pk = ScriptPubKey.from_script(txout.pk_script, self.coin)
|
||||
if not pk.hash160:
|
||||
return None
|
||||
|
||||
pack = struct.pack
|
||||
idxb = pack('<H', idx)
|
||||
txcb = pack('<I', self.tx_count)
|
||||
|
||||
# First write the hash160 lookup
|
||||
key = b'h' + tx_hash[:ADDR_TX_HASH_LEN] + idxb
|
||||
# b'' avoids this annoyance: https://bugs.python.org/issue13298
|
||||
value = b''.join([pk.hash160, txcb])
|
||||
prior_value = self.get(key)
|
||||
if prior_value: # Should almost never happen
|
||||
value += prior_value
|
||||
self.put(key, value)
|
||||
|
||||
# Next write the UTXO
|
||||
key = b'u' + pk.hash160 + tx_hash[:UTXO_TX_HASH_LEN] + idxb
|
||||
value = txcb + pack('<Q', txout.value)
|
||||
prior_value = self.get(key)
|
||||
if prior_value: # Should almost never happen
|
||||
value += prior_value
|
||||
self.put(key, value)
|
||||
|
||||
return pk.hash160
|
||||
|
||||
def open_file(self, filename, truncate=False, create=False):
|
||||
try:
|
||||
return open(filename, 'wb+' if truncate else 'rb+')
|
||||
except FileNotFoundError:
|
||||
if create:
|
||||
return open(filename, 'wb+')
|
||||
raise
|
||||
|
||||
def read_headers(self, height, count):
|
||||
header_len = self.coin.HEADER_LEN
|
||||
self.headers_file.seek(height * header_len)
|
||||
return self.headers_file.read(count * header_len)
|
||||
|
||||
def write_headers(self):
|
||||
headers = b''.join(self.headers)
|
||||
header_len = self.coin.HEADER_LEN
|
||||
assert len(headers) % header_len == 0
|
||||
self.headers_file.seek(self.db_height * header_len)
|
||||
self.headers_file.write(headers)
|
||||
self.headers_file.flush()
|
||||
self.headers = []
|
||||
|
||||
def write_tx_counts(self):
|
||||
self.txcount_file.seek(self.db_height * self.tx_counts.itemsize)
|
||||
self.txcount_file.write(self.tx_counts[self.db_height: self.height + 1])
|
||||
self.txcount_file.flush()
|
||||
|
||||
def write_tx_hashes(self):
|
||||
hash_blob = b''.join(itertools.chain(*self.tx_hashes))
|
||||
assert len(hash_blob) % 32 == 0
|
||||
assert self.tx_hash_file_size % 32 == 0
|
||||
hashes = memoryview(hash_blob)
|
||||
cursor = 0
|
||||
file_pos = self.db_tx_count * 32
|
||||
while cursor < len(hashes):
|
||||
file_num, offset = divmod(file_pos, self.tx_hash_file_size)
|
||||
size = min(len(hashes) - cursor, self.tx_hash_file_size - offset)
|
||||
filename = 'hashes{:05d}'.format(file_num)
|
||||
with self.open_file(filename, create=True) as f:
|
||||
f.seek(offset)
|
||||
f.write(hashes[cursor:cursor + size])
|
||||
cursor += size
|
||||
file_pos += size
|
||||
self.tx_hashes = []
|
||||
|
||||
def process_block(self, block):
|
||||
self.headers.append(block[:self.coin.HEADER_LEN])
|
||||
|
||||
tx_hashes, txs = self.coin.read_block(block)
|
||||
self.height += 1
|
||||
|
||||
assert len(self.tx_counts) == self.height
|
||||
|
||||
# These both need to be updated before calling process_tx().
|
||||
# It uses them for tx hash lookup
|
||||
self.tx_hashes.append(tx_hashes)
|
||||
self.tx_counts.append(self.tx_count + len(txs))
|
||||
|
||||
for tx_hash, tx in zip(tx_hashes, txs):
|
||||
self.process_tx(tx_hash, tx)
|
||||
|
||||
# Flush if we're getting full
|
||||
if len(self.write_cache) + len(self.history) > self.flush_size:
|
||||
self.flush()
|
||||
|
||||
def process_tx(self, tx_hash, tx):
|
||||
hash160s = set()
|
||||
if not tx.is_coinbase:
|
||||
for txin in tx.inputs:
|
||||
hash160s.add(self.spend_utxo(txin.prevout))
|
||||
|
||||
for idx, txout in enumerate(tx.outputs):
|
||||
hash160s.add(self.put_utxo(tx_hash, idx, txout))
|
||||
|
||||
for hash160 in hash160s:
|
||||
self.history[hash160].append(self.tx_count)
|
||||
|
||||
self.tx_count += 1
|
||||
|
||||
def get_tx_hash(self, tx_num):
|
||||
'''Returns the tx_hash and height of a tx number.'''
|
||||
height = bisect_right(self.tx_counts, tx_num)
|
||||
|
||||
# Is this on disk or unflushed?
|
||||
if height >= self.db_height:
|
||||
tx_hashes = self.tx_hashes[height - self.db_height]
|
||||
tx_hash = tx_hashes[tx_num - self.tx_counts[height - 1]]
|
||||
else:
|
||||
file_pos = tx_num * 32
|
||||
file_num, offset = divmod(file_pos, self.tx_hash_file_size)
|
||||
filename = 'hashes{:05d}'.format(file_num)
|
||||
with self.open_file(filename) as f:
|
||||
f.seek(offset)
|
||||
tx_hash = f.read(32)
|
||||
|
||||
return tx_hash, height
|
||||
|
||||
def get_balance(self, hash160):
|
||||
'''Returns the confirmed balance of an address.'''
|
||||
utxos = self.get_utxos(hash_160)
|
||||
return sum(utxo.value for utxo in utxos)
|
||||
|
||||
def get_history(self, hash160):
|
||||
'''Returns a sorted list of (tx_hash, height) tuples of transactions
|
||||
that touched the address, earliest in the blockchain first.
|
||||
Only includes outputs that have been spent. Other
|
||||
transactions will be in the UTXO set.
|
||||
'''
|
||||
prefix = b'H' + hash160
|
||||
a = array.array('I')
|
||||
for key, hist in self.db.iterator(prefix=prefix):
|
||||
a.frombytes(hist)
|
||||
return [self.get_tx_hash(tx_num) for tx_num in a]
|
||||
|
||||
def get_utxos(self, hash160):
|
||||
'''Returns all UTXOs for an address sorted such that the earliest
|
||||
in the blockchain comes first.
|
||||
'''
|
||||
unpack = struct.unpack
|
||||
prefix = b'u' + hash160
|
||||
utxos = []
|
||||
for k, v in self.db.iterator(prefix=prefix):
|
||||
(tx_pos, ) = unpack('<H', k[-2:])
|
||||
|
||||
for n in range(0, len(v), 12):
|
||||
(tx_num, ) = unpack('<I', v[n:n+4])
|
||||
(value, ) = unpack('<Q', v[n+4:n+12])
|
||||
tx_hash, height = self.get_tx_hash(tx_num)
|
||||
utxos.append(UTXO(tx_num, tx_pos, tx_hash, height, value))
|
||||
|
||||
# Sorted by height and block position.
|
||||
return sorted(utxos)
|
||||
54
server/env.py
Normal file
54
server/env.py
Normal file
@ -0,0 +1,54 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
import logging
|
||||
from os import environ
|
||||
|
||||
from lib.coins import Coin
|
||||
|
||||
|
||||
class Env(object):
|
||||
'''Wraps environment configuration.'''
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('Env')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
coin_name = self.default('COIN', 'Bitcoin')
|
||||
network = self.default('NETWORK', 'mainnet')
|
||||
self.coin = Coin.lookup_coin_class(coin_name, network)
|
||||
self.db_dir = self.required('DB_DIRECTORY')
|
||||
self.flush_size = self.integer('FLUSH_SIZE', 1000000)
|
||||
self.rpc_url = self.build_rpc_url()
|
||||
|
||||
def default(self, envvar, default):
|
||||
return environ.get(envvar, default)
|
||||
|
||||
def required(self, envvar):
|
||||
value = environ.get(envvar)
|
||||
if value is None:
|
||||
raise self.Error('required envvar {} not set'.format(envvar))
|
||||
return value
|
||||
|
||||
def integer(self, envvar, default):
|
||||
value = environ.get(envvar)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except:
|
||||
raise self.Error('cannot convert envvar {} value {} to an integer'
|
||||
.format(envvar, value))
|
||||
|
||||
def build_rpc_url(self):
|
||||
rpc_url = environ.get('RPC_URL')
|
||||
if not rpc_url:
|
||||
rpc_username = self.required('RPC_USERNAME')
|
||||
rpc_password = self.required('RPC_PASSWORD')
|
||||
rpc_host = self.required('RPC_HOST')
|
||||
rpc_port = self.default('RPC_PORT', self.coin.DEFAULT_RPC_PORT)
|
||||
rpc_url = ('http://{}:{}@{}:{}/'
|
||||
.format(rpc_username, rpc_password, rpc_host, rpc_port))
|
||||
return rpc_url
|
||||
232
server/server.py
Normal file
232
server/server.py
Normal file
@ -0,0 +1,232 @@
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from functools import partial
|
||||
|
||||
import aiohttp
|
||||
|
||||
from server.db import DB
|
||||
|
||||
|
||||
class Server(object):
|
||||
|
||||
def __init__(self, env, loop):
|
||||
self.env = env
|
||||
self.db = DB(env)
|
||||
self.rpc = RPC(env)
|
||||
self.block_cache = BlockCache(env, self.db, self.rpc, loop)
|
||||
|
||||
def async_tasks(self):
|
||||
return [
|
||||
asyncio.ensure_future(self.block_cache.catch_up()),
|
||||
asyncio.ensure_future(self.block_cache.process_cache()),
|
||||
]
|
||||
|
||||
|
||||
class BlockCache(object):
|
||||
'''Requests blocks ahead of time from the daemon. Serves them
|
||||
to the blockchain processor.'''
|
||||
|
||||
def __init__(self, env, db, rpc, loop):
|
||||
self.logger = logging.getLogger('BlockCache')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
self.db = db
|
||||
self.rpc = rpc
|
||||
self.stop = False
|
||||
# Cache target size is in MB. Has little effect on sync time.
|
||||
self.cache_limit = 10
|
||||
self.daemon_height = 0
|
||||
self.fetched_height = db.db_height
|
||||
# Blocks stored in reverse order. Next block is at end of list.
|
||||
self.blocks = []
|
||||
self.recent_sizes = []
|
||||
self.ave_size = 0
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame),
|
||||
partial(self.on_signal, signame))
|
||||
|
||||
def on_signal(self, signame):
|
||||
logging.warning('Received {} signal, preparing to shut down'
|
||||
.format(signame))
|
||||
self.blocks = []
|
||||
self.stop = True
|
||||
|
||||
async def process_cache(self):
|
||||
while not self.stop:
|
||||
await asyncio.sleep(1)
|
||||
while self.blocks:
|
||||
self.db.process_block(self.blocks.pop())
|
||||
# Release asynchronous block fetching
|
||||
await asyncio.sleep(0)
|
||||
|
||||
self.db.flush()
|
||||
|
||||
async def catch_up(self):
|
||||
self.logger.info('catching up, block cache limit {:d}MB...'
|
||||
.format(self.cache_limit))
|
||||
|
||||
while await self.maybe_prefill():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not self.stop:
|
||||
self.logger.info('caught up to height {:d}'
|
||||
.format(self.daemon_height))
|
||||
|
||||
def cache_used(self):
|
||||
return sum(len(block) for block in self.blocks)
|
||||
|
||||
def prefill_count(self, room):
|
||||
count = 0
|
||||
if self.ave_size:
|
||||
count = room // self.ave_size
|
||||
return max(count, 10)
|
||||
|
||||
async def maybe_prefill(self):
|
||||
'''Returns False to stop. True to sleep a while for asynchronous
|
||||
processing.'''
|
||||
cache_limit = self.cache_limit * 1024 * 1024
|
||||
while True:
|
||||
if self.stop:
|
||||
return False
|
||||
|
||||
cache_used = self.cache_used()
|
||||
if cache_used > cache_limit:
|
||||
return True
|
||||
|
||||
# Keep going by getting a whole new cache_limit of blocks
|
||||
self.daemon_height = await self.rpc.rpc_single('getblockcount')
|
||||
max_count = min(self.daemon_height - self.fetched_height, 4000)
|
||||
count = min(max_count, self.prefill_count(cache_limit))
|
||||
if not count or self.stop:
|
||||
return False # Done catching up
|
||||
|
||||
# self.logger.info('requesting {:,d} blocks'.format(count))
|
||||
first = self.fetched_height + 1
|
||||
param_lists = [[height] for height in range(first, first + count)]
|
||||
hashes = await self.rpc.rpc_multi('getblockhash', param_lists)
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
|
||||
# Hashes is an array of hex strings
|
||||
param_lists = [(h, False) for h in hashes]
|
||||
blocks = await self.rpc.rpc_multi('getblock', param_lists)
|
||||
self.fetched_height += count
|
||||
|
||||
if self.stop:
|
||||
return False
|
||||
|
||||
# Convert hex string to bytes and put in memoryview
|
||||
blocks = [memoryview(bytes.fromhex(block)) for block in blocks]
|
||||
# Reverse order and place at front of list
|
||||
self.blocks = list(reversed(blocks)) + self.blocks
|
||||
|
||||
self.logger.info('prefilled {:,d} blocks to height {:,d} '
|
||||
'daemon height: {:,d} block cache size: {:,d}'
|
||||
.format(count, self.fetched_height,
|
||||
self.daemon_height, self.cache_used()))
|
||||
|
||||
# Keep 50 most recent block sizes for fetch count estimation
|
||||
sizes = [len(block) for block in blocks]
|
||||
self.recent_sizes.extend(sizes)
|
||||
excess = len(self.recent_sizes) - 50
|
||||
if excess > 0:
|
||||
self.recent_sizes = self.recent_sizes[excess:]
|
||||
self.ave_size = sum(self.recent_sizes) // len(self.recent_sizes)
|
||||
|
||||
|
||||
class RPC(object):
|
||||
|
||||
def __init__(self, env):
|
||||
self.logger = logging.getLogger('RPC')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
self.rpc_url = env.rpc_url
|
||||
self.logger.info('using RPC URL {}'.format(self.rpc_url))
|
||||
|
||||
async def rpc_multi(self, method, param_lists):
|
||||
payload = [{'method': method, 'params': param_list}
|
||||
for param_list in param_lists]
|
||||
while True:
|
||||
dresults = await self.daemon(payload)
|
||||
errs = [dresult['error'] for dresult in dresults]
|
||||
if not any(errs):
|
||||
return [dresult['result'] for dresult in dresults]
|
||||
for err in errs:
|
||||
if err.get('code') == -28:
|
||||
self.logger.warning('daemon still warming up...')
|
||||
secs = 10
|
||||
break
|
||||
else:
|
||||
self.logger.error('daemon returned errors: {}'.format(errs))
|
||||
secs = 0
|
||||
self.logger.info('sleeping {:d} seconds and trying again...'
|
||||
.format(secs))
|
||||
await asyncio.sleep(secs)
|
||||
|
||||
|
||||
async def rpc_single(self, method, params=None):
|
||||
payload = {'method': method}
|
||||
if params:
|
||||
payload['params'] = params
|
||||
while True:
|
||||
dresult = await self.daemon(payload)
|
||||
err = dresult['error']
|
||||
if not err:
|
||||
return dresult['result']
|
||||
if err.get('code') == -28:
|
||||
self.logger.warning('daemon still warming up...')
|
||||
secs = 10
|
||||
else:
|
||||
self.logger.error('daemon returned error: {}'.format(err))
|
||||
secs = 0
|
||||
self.logger.info('sleeping {:d} seconds and trying again...'
|
||||
.format(secs))
|
||||
await asyncio.sleep(secs)
|
||||
|
||||
async def daemon(self, payload):
|
||||
while True:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.rpc_url,
|
||||
data=json.dumps(payload)) as resp:
|
||||
return await resp.json()
|
||||
except Exception as e:
|
||||
self.logger.error('aiohttp error: {}'.format(e))
|
||||
|
||||
self.logger.info('sleeping 1 second and trying again...')
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# for addr in [
|
||||
# # '1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp',
|
||||
# # '1HYBcza9tVquCCvCN1hUZkYT9RcM6GfLot',
|
||||
# # '1BNwxHGaFbeUBitpjy2AsKpJ29Ybxntqvb',
|
||||
# # '1ARanTkswPiVM6tUEYvbskyqDsZpweiciu',
|
||||
# # '1VayNert3x1KzbpzMGt2qdqrAThiRovi8',
|
||||
# # '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
|
||||
# # '1XPTgDRhN8RFnzniWCddobD9iKZatrvH4',
|
||||
# # '153h6eE6xRhXuN3pE53gWVfXacAtfyBF8g',
|
||||
# ]:
|
||||
# print('Address: ', addr)
|
||||
# hash160 = coin.address_to_hash160(addr)
|
||||
# utxos = self.db.get_utxos(hash160)
|
||||
# for n, utxo in enumerate(utxos):
|
||||
# print('UTXOs #{:d}: hash: {} pos: {:d} height: {:d} value: {:d}'
|
||||
# .format(n, bytes(reversed(utxo.tx_hash)).hex(),
|
||||
# utxo.tx_pos, utxo.height, utxo.value))
|
||||
|
||||
# for addr in [
|
||||
# '19k8nToWwMGuF4HkNpzgoVAYk4viBnEs5D',
|
||||
# '1HaHTfmvoUW6i6nhJf8jJs6tU4cHNmBQHQ',
|
||||
# '1XPTgDRhN8RFnzniWCddobD9iKZatrvH4',
|
||||
# ]:
|
||||
# print('Address: ', addr)
|
||||
# hash160 = coin.address_to_hash160(addr)
|
||||
# for n, (tx_hash, height) in enumerate(self.db.get_history(hash160)):
|
||||
# print('History #{:d}: hash: {} height: {:d}'
|
||||
# .format(n + 1, bytes(reversed(tx_hash)).hex(), height))
|
||||
48
server_main.py
Executable file
48
server_main.py
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# See the file "COPYING" for information about the copyright
|
||||
# and warranty status of this software.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from server.env import Env
|
||||
from server.server import Server
|
||||
|
||||
|
||||
def main_loop():
|
||||
'''Get tasks; loop until complete.'''
|
||||
if os.geteuid() == 0:
|
||||
raise Exception('DO NOT RUN AS ROOT! Create an unpriveleged user '
|
||||
'account and use that')
|
||||
|
||||
env = Env()
|
||||
logging.info('switching current directory to {}'.format(env.db_dir))
|
||||
os.chdir(env.db_dir)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
server = Server(env, loop)
|
||||
tasks = server.async_tasks()
|
||||
loop.run_until_complete(asyncio.gather(*tasks))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def main():
|
||||
'''Set up logging, enter main loop.'''
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.info('ElectrumX server starting')
|
||||
try:
|
||||
main_loop()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logging.critical('ElectrumX server terminated abnormally')
|
||||
else:
|
||||
logging.info('ElectrumX server terminated normally')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user