├── .gitignore
├── LICENCE
├── MANIFEST.in
├── README.md
├── common.spec
├── config.ini_sample
├── contrib
├── electrumpersonalserver.confd
├── electrumpersonalserver.initd
└── electrumpersonalserver.service
├── docs
├── developer-notes.md
├── pubkeys
│ └── belcher.asc
└── signed-donation-addresses.txt
├── electrum-personal-server-rescan.bat
├── electrumpersonalserver
├── __init__.py
├── bitcoin
│ ├── __init__.py
│ ├── deterministic.py
│ ├── main.py
│ ├── py2specials.py
│ ├── py3specials.py
│ ├── secp256k1_deterministic.py
│ ├── secp256k1_main.py
│ ├── secp256k1_transaction.py
│ └── transaction.py
├── certs
│ ├── cert.crt
│ └── cert.key
└── server
│ ├── __init__.py
│ ├── common.py
│ ├── deterministicwallet.py
│ ├── electrumprotocol.py
│ ├── hashes.py
│ ├── jsonrpc.py
│ ├── mempoolhistogram.py
│ ├── merkleproof.py
│ ├── peertopeer.py
│ ├── socks.py
│ └── transactionmonitor.py
├── release-notes
├── setup.cfg
├── setup.py
└── test
├── test_electrum_protocol.py
├── test_merkleproof.py
├── test_parse_mpks.py
└── test_transactionmonitor.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.swp
3 | config.ini*
4 | .cache/
5 | .pytest_cache/
6 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include doc/*.md
3 | include electrumpersonalserver/certs/*
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Electrum Personal Server
2 |
3 | Electrum Personal Server aims to make using Electrum bitcoin wallet more secure
4 | and more private. It makes it easy to connect your Electrum wallet to your own
5 | full node.
6 |
7 | [Full node](https://en.bitcoin.it/wiki/Full_node) wallets are important in
8 | bitcoin because they are a big part of what makes the system trustless. No
9 | longer do people have to trust a financial institution like a bank or Paypal,
10 | they can run software on their own computers. If bitcoin is digital gold, then
11 | a full node wallet is your own personal goldsmith who checks for you that
12 | received payments are genuine.
13 |
14 | Full node wallets are also important for privacy. Using Electrum under default
15 | configuration requires it to send (hashes of) all your bitcoin addresses to some
16 | server. That server can then easily spy on your transactions. Full node
17 | wallets like Electrum Personal Server would download the entire blockchain and
18 | scan it for the user's own addresses, and therefore don't reveal to anyone else
19 | which bitcoin addresses they are interested in.
20 |
21 | ## Contents
22 |
23 | - [Features](#features)
24 | - [Detailed how-to guide](#how-to)
25 | - [Quick start for Debian/Ubuntu](#quick-start-on-a-debianubuntu-machine-with-a-running-bitcoin-full-node)
26 | - [Links to other setup guides](#links-to-other-setup-guides)
27 | - [How to expose the server to the internet](#exposure-to-the-internet)
28 | - [How is this different from other Electrum servers ?](#how-is-this-different-from-other-electrum-servers-)
29 | - [Articles, Discussion and Talks](#articles-discussion-and-talks)
30 | - [Contributing](#contributing)
31 |
32 | ### Features
33 |
34 | - Fully-featured Electrum server for a single user. Combine full node security
35 | and privacy with all of Electrum's feature-richness: (Hardware wallet
36 | integration, [Multisignature wallets](http://docs.electrum.org/en/latest/multisig.html),
37 | [Offline signing](http://docs.electrum.org/en/latest/coldstorage.html),
38 | [Seed recovery phrases](https://en.bitcoin.it/wiki/Seed_phrase), Coin control,
39 | Fee-bumping)
40 | - Maximally lightweight. Very low CPU, RAM and disk space requirements. Only a
41 | full node required.
42 | - Compatible with all Bitcoin Core resource-saving features:
43 | - [Pruning](https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.12.0.md#wallet-pruning)
44 | - [Blocksonly](https://bitcointalk.org/index.php?topic=1377345.0)
45 | - Disabled txindex
46 | - Scriptable transaction broadcasting. When the user click "Send" the server
47 | can be configured to run a system call with the new transaction:
48 | - Broadcast transactions through Tor, for [resisting traffic analysis](https://en.bitcoin.it/wiki/Privacy#Tor_and_tor_broadcasting).
49 | - By writing a shell script (eg. `send-tx-over-sms.sh`) the server can
50 | broadcast transactions via SMS, radio or any other creative way.
51 |
52 | ## How To
53 |
54 | - If you dont already have them, download and install Bitcoin Core version 0.17
55 | or higher. Make sure you
56 | [verify the digital signatures](https://bitcoin.stackexchange.com/questions/50185/how-to-verify-bitcoin-core-release-signing-keys)
57 | of any binaries before running them, or compile from source. The Bitcoin node
58 | must have wallet functionality enabled, and must have the RPC server switched on (`server=1`
59 | in bitcoin.conf). Create a wallet dedicated to Electrum Personal Server by running
60 | `bitcoin-cli createwallet electrumpersonalserver true true "" false false true`
61 | for Bitcoin Core v23.0 and up, or
62 | `wallet=electrumpersonalserver` to the bitcoin.conf file for previous versions.
63 |
64 | - If you dont already have it, download and install
65 | [Electrum bitcoin wallet](https://electrum.org/), and set up your Electrum
66 | wallet (for example by linking your hardware wallet). To avoid damaging
67 | privacy by connecting to public Electrum servers, disconnect from the
68 | internet first or run Electrum with the command line argument
69 | `--server localhost:50002:s`. To avoid accidentally connecting to public
70 | electrum servers, also use the command line argument `--offline`.
71 |
72 | - Download the [latest release](https://github.com/chris-belcher/electrum-personal-server/releases)
73 | of Electrum Personal Server. If using Windows OS take the packaged binary
74 | release build `electrumpersonalserver-windows-release-XXX.zip`.
75 |
76 | - Extract and enter the directory, and copy the file `config.ini_sample` to
77 | `config.ini`. Edit the file `config.ini` to configure everything about the
78 | server. Add your wallet master public keys or watch-only addresses to the
79 | `[master-public-keys]` and `[watch-only-addresses]` sections. Master public
80 | keys for an Electrum wallet (which start with xpub/ypub/zpub/etc) can be found
81 | in the Electrum client menu `Wallet` -> `Information`. You can add multiple
82 | master public keys or watch-only addresses by adding separate lines for the
83 | different keys/addresses:
84 |
85 | wallet1 = xpub661MyMwAqRbcF...
86 | wallet2 = xpub7712KLsfsg46G...
87 |
88 | - If you created a wallet dedicated to Electrum Personal Server in Bitcoin Core,
89 | you have to modify the line `wallet_filename` in the `[bitcoin-rpc]` section
90 | with the name of the wallet, for example `wallet_filename = electrumpersonalserver`.
91 |
92 | - If using the windows packaged binary release, drag the file `config.ini` onto
93 | the file `electrum-personal-server.exe` to run the server, or on the command
94 | line run `electrum-personal-server config.ini`.
95 |
96 | - If installing from the source release, install Electrum Personal Server in
97 | your home directory with `pip3 install --user .`. On Linux the script
98 | `electrum-personal-server` will be installed in `~/.local/bin`. Please note,
99 | if for some reason, you want to make a system-wide install, simply run
100 | `pip3 install .` as root (e.g. if you have `sudo` setup, you could use:
101 | `sudo pip3 install .`). Run `electrum-personal-server /path/to/config.ini`
102 | to start Electrum Personal Server.
103 |
104 | - The first time the server is run it will import all configured addresses as
105 | watch-only into the Bitcoin node, and then exit.
106 | If the wallets contain historical transactions you can use the rescan script
107 | (`electrum-personal-server --rescan /path/to/config.ini`) to make them appear.
108 | If using the windows packaged binary release build then drag the file
109 | `config.ini` onto the file `electrum-personal-server-rescan.bat`.
110 |
111 | - Run the server again which will start Electrum Personal Server. Wait until
112 | the message `Listening for Electrum Wallet ...` appears and then tell
113 | Electrum to connect to the server in `Tools` -> `Server`. By default the
114 | server details are `localhost` if running on the same machine. Make sure the
115 | port number matches what is written in `config.ini` (port 50002 by default).
116 |
117 | Pro Tip: run Electrum wallet with the command line arguments `--oneserver --server localhost:50002:s`.
118 | This stops Electrum connecting to other servers to obtain block
119 | headers; and locks Electrum to connect only to your server, disabling the GUI
120 | button to stop accidental connections. This helps avoid a user accidentally
121 | ruining their privacy by connecting to public Electrum servers. Another way
122 | to do this is to open Electrum's config file and edit the lines to
123 | `oneserver=true`.
124 |
125 | Pro Tip2: run tor on the same machine as Electrum Personal Server. Then by
126 | default transactions will be broadcast through tor. If running tor, also set
127 | `walletbroadcast=0` in your `bitcoin.conf`. This prevents the node from
128 | rebroadcasting transactions without tor.
129 |
130 | ### Quick start on a Debian/Ubuntu machine with a running Bitcoin full node
131 |
132 | 1. Download the [latest release](https://github.com/chris-belcher/electrum-personal-server/releases)
133 | of Electrum Personal Server. (Not the Windows version, the "Source code" zip or
134 | tar.gz.)
135 | 1. Extract the compressed file
136 | 1. Enter the directory
137 | 1. `cp config.ini_sample config.ini`
138 | 1. Edit the config.ini file:
139 | 1. Add bitcoind back-end RPC auth information
140 | 1. Add wallet master public keys for your wallets
141 | 1. Install the server to your home directory with `pip3 install --user .`
142 | 1. Make sure `~/.local/bin` is in your \$PATH (`echo $PATH`). If not, add it:
143 | `echo 'PATH=$HOME/.local/bin:$PATH' >> ~/.profile`, logout, and log in again
144 | 1. Run the server: `electrum-personal-server config.ini`
145 | 1. Rescan if needed: `electrum-personal-server --rescan config.ini`
146 | 1. Restart the server if needed
147 | 1. Start your Electrum wallet: `electrum --oneserver --server localhost:50002:s`.
148 |
149 | ### Updating to a new version
150 |
151 | - Use `pip3 uninstall electrum-personal-server`, then use the same
152 | `pip3 install ...` command as above to install the new version.
153 |
154 | - You can always use `electrum-personal-server --version` to check which version you're running.
155 |
156 | Updating to a new version of Electrum Personal Server does not require a blockchain rescan.
157 |
158 | ### Links to other setup guides
159 |
160 | - [How to setup Electrum Personal Server on a Raspberry Pi](https://raspibolt.github.io/raspibolt/raspibolt_64_electrum.html)
161 | - [Electrum Personal Server on Windows 10](https://driftwoodpalace.github.io/Hodl-Guide/hodl-guide_63_eps-win.html)
162 | - [Running Electrum Personal Server on Mac OS](https://driftwoodpalace.github.io/Hodl-Guide/hodl-guide_64_eps-mac.html)
163 | - [How to set up your own Bitcoin node, Electrum wallet and Server](https://curiosityoverflow.xyz/posts/bitcoin-electrum-wallet/)
164 | - [How to set up Wireguard to connect to EPS](https://curiosityoverflow.xyz/posts/wireguard-eps/)
165 | - [Linux setup video tutorial on youtube](https://www.youtube.com/watch?v=1JMP4NZCC5g)
166 | - [BTCPay Server integration with Electrum Personal Server](https://docs.btcpayserver.org/ElectrumPersonalServer/)
167 | - [Using Electrum Personal Server with a Bitseed node](https://github.com/john-light/bitcoin/blob/master/eps.md)
168 | - [Spanish language video tutorial / Instalación del servidor Electrum Personal Server](https://www.youtube.com/watch?v=F3idwecYvcU)
169 | - [Japanese language setup guide](https://freefromjp.wordpress.com/2019/07/13/electrum-personal-server-%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/)
170 | - [Connect to Electrum Personal Server via Wireguard ](https://curiosityoverflow.xyz/posts/wireguard-eps/#connecting-to-electrum-personal-server)
171 |
172 | #### Exposure to the Internet
173 |
174 | Right now, Electrum Personal Server is easiest to use when it, your full node
175 | and your Electrum wallet are all on the same computer.
176 |
177 | Other people should not be connecting to your server. They won't be
178 | able to synchronize their wallet, and they could potentially learn all your
179 | wallet transactions. By default the server will accept connections only from
180 | `localhost`, though this can be changed in the configuration file.
181 |
182 | The whitelisting feature can be used accept only certain IP addresses ranges
183 | connecting to the server. The Electrum protocol uses SSL for encryption. If
184 | your wallet connects over the public internet you should generate your own
185 | SSL certificate instead of using the default one, otherwise your connection
186 | can be decrypted. See the configuration file for instruction on how to do
187 | this.
188 |
189 | Another option is to use a SSH tunnel to reach Electrum Personal Server. SSH
190 | connections are encrypted and authenticated. This can be done on the command
191 | line with: `ssh username@host -L 50002:localhost:50002` or with [Putty](https://www.putty.org/)
192 | for Windows. Then connect Electrum to localhost, and SSH will forward that
193 | connection to the server.
194 |
195 | ##### Number of connections
196 |
197 | Right now Electrum Personal Server can only accept one connection at a time.
198 |
199 | ##### Lightning Network
200 |
201 | Right now Electrum Personal Server does not support Lightning Network which
202 | Electrum wallet 4.0 and above implements.
203 |
204 | #### How is this different from other Electrum servers ?
205 |
206 | They are different approaches with different tradeoffs. Electrum Personal
207 | Server is compatible with pruning, blocksonly and txindex=0, uses less CPU and
208 | RAM, is suitable for being used intermittently rather than needing to be
209 | always-on, and doesn't require an index of every bitcoin address ever used. The
210 | tradeoff is when recovering an old wallet, you must import your wallet first
211 | and you may need to rescan, so it loses the "instant on" feature of Electrum
212 | wallet. Other Electrum server implementations will be able to sync your wallet
213 | immediately even if you have historical transactions, and they can serve
214 | multiple Electrum connections at once.
215 |
216 | Traditional Electrum servers inherently are not very scalable and use many
217 | resources which push people towards using centralized solutions. This is what
218 | we'd like to avoid with Electrum Personal Server.
219 |
220 | Definitely check out other implementations:
221 | - [ElectrumX](https://github.com/spesmilo/electrumx) - Full Electrum server maintained by the Electrum project
222 | - [Electrs](https://github.com/romanz/electrs) - Full Electrum server coded in rust
223 | - [Bitcoin Wallet Tracker](https://github.com/bwt-dev/bwt) - Wallet indexer coded in rust
224 | - [Obelisk](https://github.com/parazyd/obelisk) - Minimal Electrum server using zeromq and libbitcoin as backend
225 |
226 | #### Further ideas for work
227 |
228 | - Allowing connections from more than one Electrum instance at a time. See issue
229 | [#50](https://github.com/chris-belcher/electrum-personal-server/issues/50). First
230 | the server code should be separated from the networking code.
231 | - Fix mempool lock/CPU bottleneck issue. See issue [#96](https://github.com/chris-belcher/electrum-personal-server/issues/96).
232 | - Research and develop an easier way of rescanning the wallet when blockchain
233 | pruning is enabled. See issue [#85](https://github.com/chris-belcher/electrum-personal-server/issues/85).
234 | - Developing some way for Electrum servers to authenticate clients, so that
235 | Electrum Personal Server can accept connections from the entire internet but
236 | without a fear of privacy loss.
237 | - Dynamic adding of wallet master public keys. Perhaps by polling for changes
238 | in the config file.
239 |
240 | ## Contact
241 |
242 | I can be contacted on freenode IRC on the `#bitcoin` and `#electrum` channels,
243 | by email or on [twitter](https://twitter.com/chris_belcher_/).
244 |
245 | My PGP key fingerprint is: `0A8B 038F 5E10 CC27 89BF CFFF EF73 4EA6 77F3 1129`.
246 |
247 | ## Articles, Discussion and Talks
248 |
249 | - [BitcoinMagazine.com article](https://bitcoinmagazine.com/articles/electrum-personal-server-will-give-users-full-node-security-they-need/)
250 | - [Electrum Personal Server talk at London Bitcoin Developer Meetup](https://www.youtube.com/watch?v=uKMXYdfm-is)
251 | - Electrum Personal Server used as a building block for systems which use
252 | bitcoin without internet access. See [here](https://twitter.com/notgrubles/status/1091011511961731073)
253 | and [here](https://medium.com/hackernoon/completely-offline-bitcoin-transactions-4e58324637bd)
254 | for information and setup guide.
255 | - [Mailing list email](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-February/015707.html)
256 | - [Bitcointalk thread](https://bitcointalk.org/index.php?topic=2664747.msg27179198)
257 | - [Nasdaq article](https://www.nasdaq.com/article/the-electrum-personal-server-will-give-users-the-full-node-security-they-need-cm920443)
258 | - [Bitcoinnews.ru article (russian)](https://bitcoinnews.ru/novosti/electrum-personal-server-uluchshennaya-versiya-/)
259 | - [bits.media article (russian)](https://bits.media/razrabotchiki-electrum-opublikovali-alfa-versiyu-electrum-personal-server/)
260 |
261 | ## Contributing
262 |
263 | Donate to help improve Electrum Personal Server: `bc1qe74qzd256kxevq2gn7gmscs564lfk5tqrxqsuy`. Signed donation addresses can be found [here](/docs/signed-donation-addresses.txt).
264 |
265 | This is open source project which happily accepts coding contributions from
266 | anyone. See [developer-notes.md](docs/developer-notes.md).
267 |
--------------------------------------------------------------------------------
/common.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 | #pyinstaller config file
3 |
4 | block_cipher = None
5 |
6 |
7 | a = Analysis(['electrumpersonalserver\\server\\common.py'],
8 | pathex=['Z:\\eps'],
9 | binaries=[],
10 | datas=[],
11 | hiddenimports=[],
12 | hookspath=[],
13 | runtime_hooks=[],
14 | excludes=[],
15 | win_no_prefer_redirects=False,
16 | win_private_assemblies=False,
17 | cipher=block_cipher,
18 | noarchive=False)
19 | pyz = PYZ(a.pure, a.zipped_data,
20 | cipher=block_cipher)
21 | exe = EXE(pyz,
22 | a.scripts,
23 | a.binaries,
24 | a.zipfiles,
25 | a.datas,
26 | [],
27 | name='electrum-personal-server',
28 | debug=False,
29 | bootloader_ignore_signals=False,
30 | strip=False,
31 | upx=True,
32 | runtime_tmpdir=None,
33 | console=True )
34 |
--------------------------------------------------------------------------------
/config.ini_sample:
--------------------------------------------------------------------------------
1 | # Electrum Personal Server configuration file
2 |
3 | # Lines starting with # are called comments which are ignored.
4 | # For a line to have effect it must not start with #
5 |
6 | # The most important options are towards the top of the file
7 |
8 | [master-public-keys]
9 | # Add electrum wallet master public keys to this section
10 | # In electrum then go Wallet -> Information to get the mpk
11 |
12 | #any_name_works = xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF
13 |
14 | # Multiple master public keys may be added by simply adding another line
15 | #my_second_wallet = xpubanotherkey
16 |
17 | # Multisig wallets use format `required-signatures [list of master pub keys]`
18 | # this example is a 2-of-3 multisig wallet
19 | #multisig_wallet = 2 xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF xpub661MyMwAqRbcFseXCwRdRVkhVuzEiskg4QUp5XpUdNf2uGXvQmnD4zcofZ1MN6Fo8PjqQ5cemJQ39f7RTwDVVputHMFjPUn8VRp2pJQMgEF
20 |
21 |
22 | [bitcoin-rpc]
23 | host = 127.0.0.1
24 | port = 8332
25 | #add the bitcoin datadir to search for the .cookie file created by the
26 | # node, which avoids the need to configure rpc_user/pass
27 | #leave this option empty to have it look in the default location
28 | datadir =
29 | #if you dont want to use the .cookie method with datadir, uncomment to config u/p here
30 | #rpc_user =
31 | #rpc_password =
32 |
33 | #to be used with the multi-wallet feature
34 | # see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-0.15.0.md#multi-wallet-support
35 | # empty means default file, for when using a single wallet file
36 | wallet_filename =
37 |
38 | # how often in seconds to poll for new transactions when electrum not connected
39 | poll_interval_listening = 30
40 | # how often in seconds to poll for new transactions when electrum is connected
41 | poll_interval_connected = 1
42 |
43 | # Parameters for dealing with deterministic wallets
44 | # how many addresses to import first time, should be big because if you import too little you may have to rescan again
45 | initial_import_count = 1000
46 | # number of unused addresses kept at the head of the wallet
47 | gap_limit = 25
48 |
49 | [electrum-server]
50 | # 0.0.0.0 to accept connections from any IP
51 | #127.0.0.1 to accept from only localhost
52 | host = 127.0.0.1
53 | port = 50002
54 |
55 | # space-separated whitelist of IP addresses
56 | # accepts CIDR notation eg 192.168.0.0/16 or 2a01:4f8:1f1::/120
57 | # star (*) means all are accepted
58 | # generally requires host binding (above) to be 0.0.0.0
59 | ip_whitelist = *
60 |
61 | #SSL certificate
62 | #uses the default one, which is fine because by default nobody should be
63 | # allowed to connect to your server or scan your packets
64 | #to generate another certificate see https://github.com/spesmilo/electrum-server/blob/ce1b11d7f5f7a70a3b6cc7ec1d3e552436e54ffe/HOWTO.md#step-8-create-a-self-signed-ssl-cert
65 | certfile = certs/cert.crt
66 | keyfile = certs/cert.key
67 |
68 | # Option for disabling the fee histogram calculation
69 | # It improves server responsiveness but stops mempool-based Electrum features
70 | # This is useful on low powered devices at times when the node mempool is large
71 | disable_mempool_fee_histogram = false
72 |
73 | # How often in seconds to update the mempool
74 | # this mempool is used to calculate the mempool fee histogram
75 | mempool_update_interval = 60
76 |
77 | # Parameter for broadcasting unconfirmed transactions
78 | # Options are:
79 | # * tor-or-own-node (use tor if tor is running locally, otherwise own-node)
80 | # * own-node (broadcast using the connected full node)
81 | # * tor (broadcast to random nodes over tor)
82 | # * system <cmd> %s (save transaction to file, and invoke system command
83 | # with file path as parameter %s)
84 | broadcast_method = tor-or-own-node
85 |
86 | # For tor broadcasting (broadcast_method = tor) configure
87 | # the tor proxy host and port below
88 | tor_host = localhost
89 | tor_port = 9050
90 |
91 | # = Notes on tor broadcasting =
92 | # If using tor broadcasting, make sure you set `walletbroadcast=0` in
93 | # your bitcoin.conf file
94 | # If your full node is connected to the internet only via tor then you dont
95 | # need separate tor broadcasting
96 |
97 |
98 | [watch-only-addresses]
99 | #Add individual addresses to this section, for example paper wallets
100 | #Dont use this section for adding entire wallets, instead use the
101 | # above section `master-public-keys`
102 |
103 | #addr = 1DuqpoeTB9zLvVCXQG53VbMxvMkijk494n
104 |
105 | # A space separated list is also accepted
106 | #my_test_addresses = 3Hh7QujVLqz11tiQsnUE5CSL16WEHBmiyR 1PXRLo1FQoZyF1Jhnz4qbG5x8Bo3pFpybz bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
107 |
108 | # multiple addresses may also be added in separate lines (like master public keys above)
109 | #addr2 = 3anotheraddress
110 |
111 |
112 | [logging]
113 | # Section for configuring the logging output
114 |
115 | # The log level. Logging messages less severe than this will not be displayed
116 | #options are 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
117 | #Information printed to the log file is always at level DEBUG
118 | log_level_stdout = INFO
119 |
120 | # Location of log file, leave empty to use the default location in temp dir
121 | log_file_location =
122 |
123 | # Whether to append to log file or delete and overwrite on startup
124 | append_log = false
125 |
126 | # Format to use for logging messages
127 | #see docs https://docs.python.org/3/library/logging.html#formatter-objects
128 | log_format = %(levelname)s:%(asctime)s: %(message)s
129 |
130 |
--------------------------------------------------------------------------------
/contrib/electrumpersonalserver.confd:
--------------------------------------------------------------------------------
1 | # Copyright 1999-2021 Gentoo Authors
2 | # Distributed under the terms of the GNU General Public License v2
3 |
4 | eps_user="bitcoin"
5 | eps_group="bitcoin"
6 |
7 | eps_config="/etc/electrum-personal-server.ini"
8 |
9 | eps_args="${eps_config}"
10 |
--------------------------------------------------------------------------------
/contrib/electrumpersonalserver.initd:
--------------------------------------------------------------------------------
1 | #!/sbin/openrc-run
2 | # Copyright 1999-2021 Gentoo Authors
3 | # Distributed under the terms of the GNU General Public License v2
4 |
5 | pidfile="/var/run/electrum-personal-server.pid"
6 | command="/usr/bin/electrum-personal-server"
7 | command_args="${eps_args}"
8 | start_stop_daemon_args="-u ${eps_user} -b -m -p ${pidfile}"
9 |
10 | name="Electrum Personal Server"
11 | description="Connects to the bitcoind RPC"
12 |
13 | depend() {
14 | need bitcoind
15 | }
16 |
17 | start_pre() {
18 | if ! [ -e "${eps_config}" ]; then
19 | eerror ""
20 | eerror "Please create a configuration in ${eps_config}"
21 | eerror ""
22 | return 1
23 | fi
24 | }
25 |
--------------------------------------------------------------------------------
/contrib/electrumpersonalserver.service:
--------------------------------------------------------------------------------
1 | # Electrum Personal Server: systemd unit
2 | # /etc/systemd/system/electrumpersonalserver.service
3 |
4 | [Unit]
5 | Description=Electrum Personal Server
6 | After=bitcoind.service
7 | Requires=bitcoind.service
8 |
9 | [Service]
10 | ExecStart=/your-installation-path/server.py /your-workding-dir [optional arguments]
11 | User=bitcoin
12 | Group=bitcoin
13 | Type=simple
14 | Restart=on-failure
15 | RestartSec=60
16 | KillMode=process
17 |
18 | [Install]
19 | WantedBy=multi-user.target
20 |
--------------------------------------------------------------------------------
/docs/developer-notes.md:
--------------------------------------------------------------------------------
1 | # Developer notes for Electrum Personal Server
2 |
3 | Please keep lines under 80 characters in length and ideally don't add
4 | any external dependencies to keep this as easy to install as possible.
5 |
6 | The project tries to follow the [python style guide PEP 8](https://www.python.org/dev/peps/pep-0008/).
7 |
8 | ## Naming
9 |
10 | Do not use the acronym EPS. Acronyms are not very user-friendly and are hard to
11 | search for.
12 |
13 | ## Installing in developer mode
14 |
15 | To seamlessly work on the codebase while using `pip`, you need to
16 | install in the `develop`/`editable` mode. You can do that with:
17 |
18 | $ pip3 install --user -e /path/to/repo
19 |
20 | `/path/to/repo` can also be a relative path, so if you are in the
21 | source directory, just use `.`. This installs the scripts in the
22 | usual places, but imports the package from the source directory. This
23 | way, any changes you make are immediately visible.
24 |
25 | ## Maintainable code
26 |
27 | Read the article [How To Write Unmaintainable Code](https://github.com/Droogans/unmaintainable-code/blob/master/README.md) and do the opposite of what it says.
28 |
29 | ## Commits
30 |
31 | Commits should be [atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) and diffs should be easy to read.
32 |
33 | Commit messages should be verbose by default consisting of a short subject line
34 | (50 chars max), a blank line and detailed explanatory text as separate
35 | paragraph(s), unless the title alone is self-explanatory (like "Corrected typo
36 | in server.py") in which case a single title line is sufficient. Commit messages
37 | should be helpful to people reading your code in the future, so explain the
38 | reasoning for your decisions. Further explanation
39 | [here](https://chris.beams.io/posts/git-commit/).
40 |
41 | ## Testing
42 |
43 | Electrum Personal Server also works on [testnet](https://en.bitcoin.it/wiki/Testnet),
44 | [regtest](https://bitcoin.org/en/glossary/regression-test-mode) and
45 | [signet](https://en.bitcoin.it/wiki/Signet). The Electrum wallet can be started
46 | in testnet mode with the command line flag `--testnet`, `--regtest` or `--signet`.
47 |
48 | pytest is used for automated testing. On Debian-like systems install with
49 | `pip3 install pytest pytest-cov`
50 |
51 | Run the tests with:
52 |
53 | $ PYTHONPATH=.:$PYTHONPATH pytest
54 |
55 | Create the coverage report with:
56 |
57 | $ PYTHONPATH=.:$PYTHONPATH pytest --cov-report=html --cov
58 | $ open htmlcov/index.html
59 |
60 | If you have installed Electrum Personal Server with pip, there is no
61 | need to set `PYTHONPATH`. You could also run the tests with:
62 |
63 | $ python3 setup.py test
64 |
65 | ## Packaged binary release with pyinstaller
66 |
67 | Pyinstaller is used to create the packaged binary releases. To build run:
68 |
69 | pyinstaller common.spec
70 |
71 | This is best done on a virtual machine with the target OS installed. The
72 | `cert/` directory needs to be copied and for windows its helpful to run
73 | `unix2dos config.ini_sample` to convert the line endings.
74 |
75 |
--------------------------------------------------------------------------------
/docs/pubkeys/belcher.asc:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP PUBLIC KEY BLOCK-----
2 | Version: SKS 1.1.5
3 |
4 | mQINBFPk74oBEACzBLjd+Z5z7eimqPuObFTaJCTXP7fgZjgVwt+q94VQ2wM0ctk/Ft9w2A92
5 | f14T7PiHaVDjHxrcW+6sw2VI2f60T8Tjf+b4701hIybluWL8DntG9BW19bZLmjAj7zkgektl
6 | YNDUrlYcQq2OEHm/MGk6Ajt2RA56aRKqoz22e+4ZA89gDgamxUAadul7AETSsgqOEUDI0FKR
7 | FODzoH65w1ien/DLkG1f76jd0XA6AxrESJVO0JzvkTnJGElBcA37rYaMmDi4DhG2MY4u63VE
8 | 8h6DyUXcRhmTZIAj+r+Ht+KMDiuiyQcKywCzzF/7Ui7YxqeAgjm5aPDU2E8X9Qd7cqHQzFM7
9 | ZCqc9P6ENAk5a0JjHw0d0knApboSvkIJUB0j1xDIS0HaRlfHM4TPdOoDgnaXb7BvDfE+0zSz
10 | WkvAns9oJV6uWdnz5kllVCjgB/FXO4plyFCHhXikXjm1XuQyL8xV88OqgDFXwVhKrDL9Pknu
11 | sTchYm3BS2b5Xq1HQqToT3I2gRGTtDzZVZV0izCefJaDp1mf49k2cokDEfw9MroEj4A0Wfht
12 | 0J64pzlBYn/9zor5cZp/EAblLRDK6HKhSZArIiDR1RC7a6s7oTzmfn0suhKDdTzkbTAnDsPi
13 | Dokl58xoxz+JdYKjzVh98lpcvMPlbZ+LwIsgbdH4KZj7mVOsJwARAQABtB9DaHJpcyBCZWxj
14 | aGVyIDxmYWxzZUBlbWFpbC5jb20+iQEcBBMBAgAGBQJWMUYAAAoJELOuCfHpoxl61jUH/jdG
15 | SozE6+JCokJJ5Mx9HhPp/+5TgBHC00ykLe/Es3q84q1/mj3ovpoO44OTbJjIrZvR5k85c/gT
16 | Mn3aQQMjl3pI/fpugIub+Nq7v6lSO1akn/r8DEFLzr4MSCy3yofe307JvwVxn9EFTubgr1dn
17 | 8OAxshQwA+mSsoMgpuoKfgU7T/4x5Kjg11l1QiuMq20/4IglsSSo3vsvoSt7LEGf7NnpLk/b
18 | LRU8MatkYsgKGcuKs61MFyKkxv3mULsllObDqRxsmOKocMk7/rp/7ozMQNrtHB2MDgOQmrMJ
19 | wIVZqAHCFP7holgPACAscH26eq/KHU2BEETDgkM4ThvaNwUmIhCJAhwEEAECAAYFAlYe6GEA
20 | CgkQJHQyDqNnlYAhcg/8CJ/DZvGqfWNEASPGPYuZMEkNrCilpskPqyaD0jdXQvaJgFIWlQBV
21 | Wl+tknG9Iuz6l0kWfIMS/KIpzBxAoB8FC/TtIoJWGRCagFZHVjtaKr2oJQLqAqXiSRCadsWA
22 | TN16nfdipXuixi6xTVOhxvI4K2QKvQv2Qm3nx6NeyipeOUCe3UdtaPgQttKMt0BqhjMPTn1o
23 | oBDG0Whc28WdBJswa69S5VjkeFNrE8gSTDmqXasz4QwprGYN/A3wImzrh19IJnz5zdNW0WoN
24 | /Xf1zd5XctVSmhJ3Jdrch7vnAWfGI0oD3brYZzUTc6UJq/JaORXhqH7toxxc07YSg4Bj0j53
25 | m0LbaWmu0yut2Te8LU6CjMol1RFr57BRC2dOGtege0t6MjRdXptGlAKtNkMlr8ndEkCCrFIG
26 | C/UMtI+nSVdusVR35MJaHRXFlPNvxbHfPZhpMO7Otzq5yfk8lSOxDny9V/6929dRoMKP9zBy
27 | OhzJib8o2/ykv1zFd5aouwjD/jhIj2r4Xy5MdRac9MsLMz8A1hVHUUS69Gmn0csY857WpdFf
28 | R4thh6iqL2yd+ngXKveFHk06fm9MUd4vsEjQWW/PmY3FQlB/Gs25IjK3Di49V/LstrEqSfWk
29 | hWA4t23c6sinZSaIbCKcwARC/x5SpkB7JszB5HH0hDPFqQ+PC0lZyoqJAhwEEAEKAAYFAlZG
30 | pcgACgkQG5GE354Rdxi8Rw/+MbF00L6S95ftPwBqeuvY/ywccYXFuXb6epNOT0wEJnmDKIW4
31 | /ASjFFMofdYyrSSW/Iw5WKPZtnGxXBN1dx8YOhT7tGK0bGHYDKFmVAebfU/RNK5kJrn5TjXl
32 | sM8qmU8CVqsIL6rJdGC6F0TU4NTZxQVosQqUZrouRifbhMvWGKCpNNCGuwH+3FgoBqoiFwTw
33 | qpyQYOpUFNmVYKioljMumHPgIjAu79XsqwvzumAuBmYS9tu+ZJxEZvk1r73gQUX3Ub0Nlcp+
34 | EZyhKFUvM1MVaP8eeNZl+QZjIrkGOg6osKgvkTnx0y5aDmwaMJ8crsVPU8XV5GDjbPWcCZyl
35 | OX9CxyixC7hyCxlju4w1TxZV6RUaksst+9G1fDCYbyh6H7HZIPDg74R52MohdvRpoSV8Bymo
36 | DGWOVkgwVbB+Dj47nmerp5pHDChf3XXfnyzWgeR06jTTvNraUrulGnvsIstJW1UUFdQUM28E
37 | HahqOB6KMvSZcaNzjPXiR+hlfF/jqE1JPGfXvF29kPjbySjIzqp2MI9EmsQFdcm16LmjUykI
38 | JqZTr0o4/vmF237LoW1Mio7aGW7afbWpwteWui/OnhrI08Dh5kfkBBgwdgjJUumVfjYXknwT
39 | VYAw3keXH4AQn43Br9/4Q67DYDY/7Olgu2isvxy8+Jwu74lu57epG0PC4q+JAj4EEwECACgF
40 | AlPk74oCGwMFCRLMAwAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEO9zTqZ38xEpLyMP
41 | /0/19JhIhWOW0v1usAx8i7vAmfQfSVdNty4Iza5CK0ncE3Ejdztqrl0RvLbu7lR3nii2dwMm
42 | 5E9BUMqzofu3WsieK2AnUxND39pAYUBu0pyC1UgwVt4KXNNN9avhbt8Tjk0jsT0QA2CTkl+Z
43 | aHdT4og1gsXlG2rKdihyuw/rM5X4b0AcKkF6Z31M3rQKgwszIzNMcRbWhB99pUBdjwOC2ERg
44 | 7+h1Zgih3tVOYSbTNTVpLKw+ozA9Y2bxciXR/xJ8pDJmUBTx7V4IVUJ6eQTjgUoLmpA1hzZT
45 | F7ERi18JYKFMZzigcrEjJiZMIJ5+xAUu+DC2+U0dFcdIMpI9I1xFymXwMWk0QquYyWQnwzH5
46 | hHTkLJ2VNDRsjcUGiEkL5QeuMPkjeEDAlQLWj7JVI4R37M3dwbUQQIjCQRBKYGAbm3+V7ZF3
47 | N9c52l/2S7b/OgGmrTRHngnFQqV9Ezs49O/tf7UAoeTE7JJ0JnAaC92HZaG6qziRO+1yjTSA
48 | IUz18F3miLVEm4e3EeyPz79dTXlQi1uN6j3oIiUzlhR6hEs9PmbFlhaBgABEV5IVF42r9O7C
49 | r13wSiRSu0DKs9rLOeWLLndQl9YNXOX3blzQLPQdZo/Z9aP64VyP/wrRuGm25YlDLQ+iG3XP
50 | dokLVSMSCvVfPwjuN5ImYiUD2mVwnDaTh2FZuQINBFPk74oBEADIXEAzbwRDAty2vQKtCFhZ
51 | l8wrMCELD4TKtQn797i6IBDg7JeJ9GtlXqQtG6XdZtMHWSkDrCAJKE7Q3e/Dx8UbYKzpCmFV
52 | qAQfzJ9BSuhpXGAANg5Mscw5vsg1Lysoj+5E2YTptdNfbuhqoIRcszJypiW59p3uGvjZL9Pz
53 | M2Hmba+XpP/U+iPb+iuZWIrADEz7yp/F3wU0jhQqarbNkT2Znk+JIhfSpGAzTIK/FXf17H+e
54 | HHOi5MG+/gtvWe2mVH9swusfGwLOHIwoOBxfsCkab7LpgSw6/PD2tEJG4mkTiW2HxKGCNO01
55 | Md+SUEPpm3KZzurtt09ako4S87mUNDqrz4p8Q2/zrZw4uVYuTthHb6YdnU9caFYLuCDASR8Y
56 | mhejTHxf7OBQp7b70mPjISF8vQhsVgLPYnl35xZUgGS0jfGqTAtRla4BsTe+BgDiHzO1HqXX
57 | w1M+SIC+k1ARovhlGcDHbZZ2xDwaURZ3IpkFZstR01wyY98aLkULDac28LI3qS2/mZNkTXYS
58 | qC+nNhy5QZA8gxX0pOVI+6bc0sOrxAjqCtsbyI02kQHGYJA4rPb8G5gClTaHC3YTjh8vx7oJ
59 | EreuJ1xF2qm56DvmbLoqQ6qbydKIwRz+lUgfB0n3+q6K5Fp4L+1laVg8jANaBvxkT+DrrPfl
60 | YCC4xD4TB++ABQARAQABiQIlBBgBAgAPBQJT5O+KAhsMBQkSzAMAAAoJEO9zTqZ38xEppnAP
61 | /04jejeBWay3+YbHqFVCvL76G3x0VY0iIBI8G8PcTkRZupaLqaapIg5LoLGNTQ+dzFME8Je3
62 | PGj9bO4cZCD/G015DRPxvaUFJxkDD3eqbUhwdctR7nD9NqnphoOZJgIhkB21sfNh89bgkrty
63 | a9yO8XyOFMU4EQ4VVCROLX0btxpWi3T7AvV7YNEcNcB7kKKcSlosGXAd4S7/mPrBzd6uiFRm
64 | Ur0hsByWPiJlNBtqrcTKpReXn10E443GYvl50nHNWKyEhhAYjID1Y/VLKVe36Q7zY0SpkbY7
65 | F1cCXOO4nwLakAb6dsu0OjiFyyI38wBpf38kHsxdyVPmKh6sXjI2AoR1+rsubKexNR3zD8iX
66 | KnsAP4Fai6PI73iLyH14q+N/0+9LDFC4CNGnGd7tkkPy8Kww1cEdJt4QrYuLBK0cv/Djve+J
67 | ekMPncno4vFfprlG6aZH6uLFswft2AowlDsWXIOmGbeivoJKQYSKvUoDTLW+pA0W0eGC4Vqk
68 | BYwMpomNBA2C9lSY6jqFtmqa9QXU5Zx3oYDqPdY5R7d8Mld9uro/0mbGrriinzzt1gqyDvGN
69 | Cr/sozvDJFypddxsF2C821UM/4SfME0fJhj/eazhggcTkkfFD0tXjI9xaRhO9vVek0gVVutJ
70 | cB7IYdU1P5I7qiUWi9tYgLcDP3J6xozVpKhO
71 | =jSee
72 | -----END PGP PUBLIC KEY BLOCK-----
73 |
--------------------------------------------------------------------------------
/docs/signed-donation-addresses.txt:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP SIGNED MESSAGE-----
2 | Hash: SHA512
3 |
4 | Bitcoin address for sending donations to the Electrum Personal Server project:
5 |
6 | bc1qe74qzd256kxevq2gn7gmscs564lfk5tqrxqsuy
7 |
8 | Always check signatures of addresses to avoid man-in-the-middle attacks
9 | The GPG public key and fingerprint can be found at:
10 | * The Electrum Personal Server github repository in /docs/pubkeys/belcher.asc
11 | * https://github.com/chris-belcher
12 | * https://www.reddit.com/r/publickeyexchange/comments/3ti8vp/ubelcher_s_public_key/
13 | * https://twitter.com/chris_belcher_/status/559879313622061056
14 | * https://bitcointalk.org/index.php?action=profile;u=321816
15 | * https://en.bitcoin.it/wiki/User:Belcher
16 | * https://x0f.org/@belcher/105595837557120598
17 |
18 | -----BEGIN PGP SIGNATURE-----
19 |
20 | iQJEBAEBCgAuFiEECosDj14QzCeJv8//73NOpnfzESkFAmImWOcQHGZhbHNlQGVt
21 | YWlsLmNvbQAKCRDvc06md/MRKW29D/9QJMY4egYSlSUtKVUyE7AiWTQeG/ClWE2S
22 | oOOKrneoQ+dEXcUA5o1P5+I8i6h03cwTQraZyL3sN+01X65d6vv57Qp11B2OQb4h
23 | 6Xz4TYK7uUGc4oC/OkHPiM2XUsfH10YNZyYx6pvGTYURlzf7iyhysWaaGYHWmlmK
24 | ksTBNopx/S840ChT+h0M3zqQoEGodphZ27CTzv+ruaVhOL1X1H5Em6/H7VTp0yyu
25 | BL8pTvt6LP7Ca71bhDHllIOuAQy1OeWgcpeSn2kD9JNK47q8j+yX4G9BN2BxKEUx
26 | xPC6sUO/qtigN1X7E4EmlHBXlbB0mnyGPVQAeoetgwpz8/psay5MPTo+MYfP/t4i
27 | iDgELdqWoGnsd+nM89GIkTH+LjQounyzhKqYEsAubV8Vz3J1xiV+qyf2w2rvPY9g
28 | hJpMmJI3tXN/hGPv7b+hcQ+iV7oHXPtHta55w9DhCC9a29t/4Z7Pe2gthewklD48
29 | gLBZOr6ZLFbJVphqArFc/qfDElgOmLxxVPm9GvzFPi7Hc/2BJNmV+Nn3WZwrdl1w
30 | RmnqhQr3nEivB2zpBYtTwTPqP6OwhSE9BqqnM6dWoYssv6DqdpcMzrjp5skuC+JV
31 | YV0nitJdDi+7tYiouVCzaR/XuzXImwn0DoOJkEljZw1ytU+b/M4CzYjOJ8+XhEud
32 | ujzBkPVxVA==
33 | =FNv/
34 | -----END PGP SIGNATURE-----
35 |
36 |
--------------------------------------------------------------------------------
/electrum-personal-server-rescan.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | electrum-personal-server --rescan %1
3 |
--------------------------------------------------------------------------------
/electrumpersonalserver/__init__.py:
--------------------------------------------------------------------------------
1 | import electrumpersonalserver.bitcoin
2 | import electrumpersonalserver.server
3 |
4 | __certfile__ = 'certs/cert.crt'
5 | __keyfile__ = 'certs/cert.key'
6 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/__init__.py:
--------------------------------------------------------------------------------
1 | from electrumpersonalserver.bitcoin.py2specials import *
2 | from electrumpersonalserver.bitcoin.py3specials import *
3 |
4 | secp_present = False
5 | try:
6 | import secp256k1
7 |
8 | secp_present = True
9 | from electrumpersonalserver.bitcoin.secp256k1_main import *
10 | from electrumpersonalserver.bitcoin.secp256k1_transaction import *
11 | from electrumpersonalserver.bitcoin.secp256k1_deterministic import *
12 | except (ImportError, AttributeError) as e:
13 | from electrumpersonalserver.bitcoin.main import *
14 | from electrumpersonalserver.bitcoin.deterministic import *
15 | from electrumpersonalserver.bitcoin.transaction import *
16 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/deterministic.py:
--------------------------------------------------------------------------------
1 | from electrumpersonalserver.bitcoin.main import *
2 | import hmac
3 | import hashlib
4 | from binascii import hexlify
5 |
6 | # Below code ASSUMES binary inputs and compressed pubkeys
7 | MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
8 | #MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
9 | TESTNET_PRIVATE = b'\x04\x35\x83\x94'
10 | #TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
11 | PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE]
12 | #PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC]
13 |
14 | #updated for electrum's bip32 version bytes
15 | #only public keys because electrum personal server only needs them
16 | #https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
17 | PUBLIC = [ b'\x04\x88\xb2\x1e', #mainnet p2pkh or p2sh xpub
18 | b'\x04\x9d\x7c\xb2', #mainnet p2wpkh-p2sh ypub
19 | b'\x02\x95\xb4\x3f', #mainnet p2wsh-p2sh Ypub
20 | b'\x04\xb2\x47\x46', #mainnet p2wpkh zpub
21 | b'\x02\xaa\x7e\xd3', #mainnet p2wsh Zpub
22 | b'\x04\x35\x87\xcf', #testnet p2pkh or p2sh tpub
23 | b'\x04\x4a\x52\x62', #testnet p2wpkh-p2sh upub
24 | b'\x02\x42\x89\xef', #testnet p2wsh-p2sh Upub
25 | b'\x04\x5f\x1c\xf6', #testnet p2wpkh vpub
26 | b'\x02\x57\x54\x83' #testnet p2wsh Vpub
27 | ]
28 |
29 | # BIP32 child key derivation
30 |
31 | def raw_bip32_ckd(rawtuple, i):
32 | vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple
33 | i = int(i)
34 |
35 | if vbytes in PRIVATE:
36 | priv = key
37 | pub = privtopub(key)
38 | else:
39 | pub = key
40 |
41 | if i >= 2**31:
42 | if vbytes in PUBLIC:
43 | raise ValueError("Can't do private derivation on public key!")
44 | I = hmac.new(chaincode, b'\x00' + priv[:32] + encode(i, 256, 4),
45 | hashlib.sha512).digest()
46 | else:
47 | I = hmac.new(chaincode, pub + encode(i, 256, 4),
48 | hashlib.sha512).digest()
49 |
50 | if vbytes in PRIVATE:
51 | newkey = add_privkeys(I[:32] + B'\x01', priv)
52 | fingerprint = bin_hash160(privtopub(key))[:4]
53 | if vbytes in PUBLIC:
54 | newkey = add_pubkeys(compress(privtopub(I[:32])), key)
55 | fingerprint = bin_hash160(key)[:4]
56 |
57 | return (vbytes, depth + 1, fingerprint, i, I[32:], newkey)
58 |
59 |
60 | def bip32_serialize(rawtuple):
61 | vbytes, depth, fingerprint, i, chaincode, key = rawtuple
62 | i = encode(i, 256, 4)
63 | chaincode = encode(hash_to_int(chaincode), 256, 32)
64 | keydata = b'\x00' + key[:-1] if vbytes in PRIVATE else key
65 | bindata = vbytes + from_int_to_byte(
66 | depth % 256) + fingerprint + i + chaincode + keydata
67 | return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58)
68 |
69 |
70 | def bip32_deserialize(data):
71 | dbin = changebase(data, 58, 256)
72 | if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]:
73 | raise ValueError("Invalid checksum")
74 | vbytes = dbin[0:4]
75 | depth = from_byte_to_int(dbin[4])
76 | fingerprint = dbin[5:9]
77 | i = decode(dbin[9:13], 256)
78 | chaincode = dbin[13:45]
79 | key = dbin[46:78] + b'\x01' if vbytes in PRIVATE else dbin[45:78]
80 | return (vbytes, depth, fingerprint, i, chaincode, key)
81 |
82 |
83 | def raw_bip32_privtopub(rawtuple):
84 | vbytes, depth, fingerprint, i, chaincode, key = rawtuple
85 | newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
86 | return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key))
87 |
88 |
89 | def bip32_privtopub(data):
90 | return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data)))
91 |
92 |
93 | def bip32_ckd(data, i):
94 | return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i))
95 |
96 |
97 | def bip32_master_key(seed, vbytes=MAINNET_PRIVATE):
98 | I = hmac.new(
99 | from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest()
100 | return bip32_serialize((vbytes, 0, b'\x00' * 4, 0, I[32:], I[:32] + b'\x01'
101 | ))
102 |
103 |
104 | def bip32_bin_extract_key(data):
105 | return bip32_deserialize(data)[-1]
106 |
107 |
108 | def bip32_extract_key(data):
109 | return safe_hexlify(bip32_deserialize(data)[-1])
110 |
111 |
112 | def bip32_descend(*args):
113 | if len(args) == 2:
114 | key, path = args
115 | else:
116 | key, path = args[0], map(int, args[1:])
117 | for p in path:
118 | key = bip32_ckd(key, p)
119 | return bip32_extract_key(key)
120 |
121 | # electrum
122 | def electrum_stretch(seed):
123 | return slowsha(seed)
124 |
125 | # Accepts seed or stretched seed, returns master public key
126 |
127 | def electrum_mpk(seed):
128 | if len(seed) == 32:
129 | seed = electrum_stretch(seed)
130 | return privkey_to_pubkey(seed)[2:]
131 |
132 | # Accepts (seed or stretched seed), index and secondary index
133 | # (conventionally 0 for ordinary addresses, 1 for change) , returns privkey
134 |
135 |
136 | def electrum_privkey(seed, n, for_change=0):
137 | if len(seed) == 32:
138 | seed = electrum_stretch(seed)
139 | mpk = electrum_mpk(seed)
140 | offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+
141 | from_int_representation_to_bytes(for_change)+b':'+
142 | binascii.unhexlify(mpk))
143 | return add_privkeys(seed, offset)
144 |
145 | # Accepts (seed or stretched seed or master pubkey), index and secondary index
146 | # (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey
147 |
148 | def electrum_pubkey(masterkey, n, for_change=0):
149 | if len(masterkey) == 32:
150 | mpk = electrum_mpk(electrum_stretch(masterkey))
151 | elif len(masterkey) == 64:
152 | mpk = electrum_mpk(masterkey)
153 | else:
154 | mpk = masterkey
155 | bin_mpk = encode_pubkey(mpk, 'bin_electrum')
156 | offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+
157 | from_int_representation_to_bytes(for_change)+b':'+bin_mpk)
158 | return add_pubkeys('04'+mpk, privtopub(offset))
159 |
160 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | from .py2specials import *
3 | from .py3specials import *
4 | import binascii
5 | import hashlib
6 | import re
7 | import sys
8 | import os
9 | import base64
10 | import time
11 | import random
12 | import hmac
13 |
14 | is_python2 = sys.version_info.major == 2
15 |
16 | # Elliptic curve parameters (secp256k1)
17 |
18 | P = 2**256 - 2**32 - 977
19 | N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
20 | A = 0
21 | B = 7
22 | Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
23 | Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424
24 | G = (Gx, Gy)
25 |
26 | # Extended Euclidean Algorithm
27 | def inv(a, n):
28 | lm, hm = 1, 0
29 | low, high = a % n, n
30 | while low > 1:
31 | r = high // low
32 | nm, new = hm - lm * r, high - low * r
33 | lm, low, hm, high = nm, new, lm, low
34 | return lm % n
35 |
36 | # Elliptic curve Jordan form functions
37 | # P = (m, n, p, q) where m/n = x, p/q = y
38 |
39 | def isinf(p):
40 | return p[0] == 0 and p[1] == 0
41 |
42 |
43 | def jordan_isinf(p):
44 | return p[0][0] == 0 and p[1][0] == 0
45 |
46 |
47 | def mulcoords(c1, c2):
48 | return (c1[0] * c2[0] % P, c1[1] * c2[1] % P)
49 |
50 |
51 | def mul_by_const(c, v):
52 | return (c[0] * v % P, c[1])
53 |
54 |
55 | def addcoords(c1, c2):
56 | return ((c1[0] * c2[1] + c2[0] * c1[1]) % P, c1[1] * c2[1] % P)
57 |
58 |
59 | def subcoords(c1, c2):
60 | return ((c1[0] * c2[1] - c2[0] * c1[1]) % P, c1[1] * c2[1] % P)
61 |
62 |
63 | def invcoords(c):
64 | return (c[1], c[0])
65 |
66 |
67 | def jordan_add(a, b):
68 | if jordan_isinf(a):
69 | return b
70 | if jordan_isinf(b):
71 | return a
72 |
73 | if (a[0][0] * b[0][1] - b[0][0] * a[0][1]) % P == 0:
74 | if (a[1][0] * b[1][1] - b[1][0] * a[1][1]) % P == 0:
75 | return jordan_double(a)
76 | else:
77 | return ((0, 1), (0, 1))
78 | xdiff = subcoords(b[0], a[0])
79 | ydiff = subcoords(b[1], a[1])
80 | m = mulcoords(ydiff, invcoords(xdiff))
81 | x = subcoords(subcoords(mulcoords(m, m), a[0]), b[0])
82 | y = subcoords(mulcoords(m, subcoords(a[0], x)), a[1])
83 | return (x, y)
84 |
85 |
86 | def jordan_double(a):
87 | if jordan_isinf(a):
88 | return ((0, 1), (0, 1))
89 | num = addcoords(mul_by_const(mulcoords(a[0], a[0]), 3), (A, 1))
90 | den = mul_by_const(a[1], 2)
91 | m = mulcoords(num, invcoords(den))
92 | x = subcoords(mulcoords(m, m), mul_by_const(a[0], 2))
93 | y = subcoords(mulcoords(m, subcoords(a[0], x)), a[1])
94 | return (x, y)
95 |
96 |
97 | def jordan_multiply(a, n):
98 | if jordan_isinf(a) or n == 0:
99 | return ((0, 0), (0, 0))
100 | if n == 1:
101 | return a
102 | if n < 0 or n >= N:
103 | return jordan_multiply(a, n % N)
104 | if (n % 2) == 0:
105 | return jordan_double(jordan_multiply(a, n // 2))
106 | if (n % 2) == 1:
107 | return jordan_add(jordan_double(jordan_multiply(a, n // 2)), a)
108 |
109 |
110 | def to_jordan(p):
111 | return ((p[0], 1), (p[1], 1))
112 |
113 |
114 | def from_jordan(p):
115 | return (p[0][0] * inv(p[0][1], P) % P, p[1][0] * inv(p[1][1], P) % P)
116 |
117 | def fast_multiply(a, n):
118 | return from_jordan(jordan_multiply(to_jordan(a), n))
119 |
120 |
121 | def fast_add(a, b):
122 | return from_jordan(jordan_add(to_jordan(a), to_jordan(b)))
123 |
124 | # Functions for handling pubkey and privkey formats
125 |
126 |
127 | def get_pubkey_format(pub):
128 | if is_python2:
129 | two = '\x02'
130 | three = '\x03'
131 | four = '\x04'
132 | else:
133 | two = 2
134 | three = 3
135 | four = 4
136 |
137 | if isinstance(pub, (tuple, list)): return 'decimal'
138 | elif len(pub) == 65 and pub[0] == four: return 'bin'
139 | elif len(pub) == 130 and pub[0:2] == '04': return 'hex'
140 | elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed'
141 | elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed'
142 | elif len(pub) == 64: return 'bin_electrum'
143 | elif len(pub) == 128: return 'hex_electrum'
144 | else: raise Exception("Pubkey not in recognized format")
145 |
146 |
147 | def encode_pubkey(pub, formt):
148 | if not isinstance(pub, (tuple, list)):
149 | pub = decode_pubkey(pub)
150 | if formt == 'decimal': return pub
151 | elif formt == 'bin':
152 | return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32)
153 | elif formt == 'bin_compressed':
154 | return from_int_to_byte(2 + (pub[1] % 2)) + encode(pub[0], 256, 32)
155 | elif formt == 'hex':
156 | return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64)
157 | elif formt == 'hex_compressed':
158 | return '0' + str(2 + (pub[1] % 2)) + encode(pub[0], 16, 64)
159 | elif formt == 'bin_electrum':
160 | return encode(pub[0], 256, 32) + encode(pub[1], 256, 32)
161 | elif formt == 'hex_electrum':
162 | return encode(pub[0], 16, 64) + encode(pub[1], 16, 64)
163 | else:
164 | raise Exception("Invalid format!")
165 |
166 |
167 | def decode_pubkey(pub, formt=None):
168 | if not formt: formt = get_pubkey_format(pub)
169 | if formt == 'decimal': return pub
170 | elif formt == 'bin':
171 | return (decode(pub[1:33], 256), decode(pub[33:65], 256))
172 | elif formt == 'bin_compressed':
173 | x = decode(pub[1:33], 256)
174 | beta = pow(int(x * x * x + A * x + B), int((P + 1) // 4), int(P))
175 | y = (P - beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta
176 | return (x, y)
177 | elif formt == 'hex':
178 | return (decode(pub[2:66], 16), decode(pub[66:130], 16))
179 | elif formt == 'hex_compressed':
180 | return decode_pubkey(safe_from_hex(pub), 'bin_compressed')
181 | elif formt == 'bin_electrum':
182 | return (decode(pub[:32], 256), decode(pub[32:64], 256))
183 | elif formt == 'hex_electrum':
184 | return (decode(pub[:64], 16), decode(pub[64:128], 16))
185 | else:
186 | raise Exception("Invalid format!")
187 |
188 |
189 | def get_privkey_format(priv):
190 | if isinstance(priv, int_types): return 'decimal'
191 | elif len(priv) == 32: return 'bin'
192 | elif len(priv) == 33: return 'bin_compressed'
193 | elif len(priv) == 64: return 'hex'
194 | elif len(priv) == 66: return 'hex_compressed'
195 | else:
196 | bin_p = b58check_to_bin(priv)
197 | if len(bin_p) == 32: return 'wif'
198 | elif len(bin_p) == 33: return 'wif_compressed'
199 | else: raise Exception("WIF does not represent privkey")
200 |
201 |
202 | def encode_privkey(priv, formt, vbyte=0):
203 | if not isinstance(priv, int_types):
204 | return encode_privkey(decode_privkey(priv), formt, vbyte)
205 | if formt == 'decimal': return priv
206 | elif formt == 'bin': return encode(priv, 256, 32)
207 | elif formt == 'bin_compressed': return encode(priv, 256, 32) + b'\x01'
208 | elif formt == 'hex': return encode(priv, 16, 64)
209 | elif formt == 'hex_compressed': return encode(priv, 16, 64) + '01'
210 | elif formt == 'wif':
211 | return bin_to_b58check(encode(priv, 256, 32), 128 + int(vbyte))
212 | elif formt == 'wif_compressed':
213 | return bin_to_b58check(
214 | encode(priv, 256, 32) + b'\x01', 128 + int(vbyte))
215 | else:
216 | raise Exception("Invalid format!")
217 |
218 |
219 | def decode_privkey(priv, formt=None):
220 | if not formt: formt = get_privkey_format(priv)
221 | if formt == 'decimal': return priv
222 | elif formt == 'bin': return decode(priv, 256)
223 | elif formt == 'bin_compressed': return decode(priv[:32], 256)
224 | elif formt == 'hex': return decode(priv, 16)
225 | elif formt == 'hex_compressed': return decode(priv[:64], 16)
226 | elif formt == 'wif': return decode(b58check_to_bin(priv), 256)
227 | elif formt == 'wif_compressed':
228 | return decode(b58check_to_bin(priv)[:32], 256)
229 | else:
230 | raise Exception("WIF does not represent privkey")
231 |
232 |
233 | def add_pubkeys(p1, p2):
234 | f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2)
235 | return encode_pubkey(
236 | fast_add(
237 | decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1)
238 |
239 |
240 | def add_privkeys(p1, p2):
241 | f1, f2 = get_privkey_format(p1), get_privkey_format(p2)
242 | return encode_privkey(
243 | (decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1)
244 |
245 |
246 | def multiply(pubkey, privkey):
247 | f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey)
248 | pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2)
249 | # http://safecurves.cr.yp.to/twist.html
250 | if not isinf(pubkey) and (
251 | pubkey[0]**3 + B - pubkey[1] * pubkey[1]) % P != 0:
252 | raise Exception("Point not on curve")
253 | return encode_pubkey(fast_multiply(pubkey, privkey), f1)
254 |
255 |
256 | def divide(pubkey, privkey):
257 | factor = inv(decode_privkey(privkey), N)
258 | return multiply(pubkey, factor)
259 |
260 |
261 | def compress(pubkey):
262 | f = get_pubkey_format(pubkey)
263 | if 'compressed' in f: return pubkey
264 | elif f == 'bin':
265 | return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed')
266 | elif f == 'hex' or f == 'decimal':
267 | return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed')
268 |
269 |
270 | def decompress(pubkey):
271 | f = get_pubkey_format(pubkey)
272 | if 'compressed' not in f: return pubkey
273 | elif f == 'bin_compressed':
274 | return encode_pubkey(decode_pubkey(pubkey, f), 'bin')
275 | elif f == 'hex_compressed' or f == 'decimal':
276 | return encode_pubkey(decode_pubkey(pubkey, f), 'hex')
277 |
278 |
279 | def privkey_to_pubkey(privkey):
280 | f = get_privkey_format(privkey)
281 | privkey = decode_privkey(privkey, f)
282 | if privkey >= N:
283 | raise Exception("Invalid privkey")
284 | if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']:
285 | return encode_pubkey(fast_multiply(G, privkey), f)
286 | else:
287 | return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex'))
288 |
289 |
290 | privtopub = privkey_to_pubkey
291 |
292 |
293 | def privkey_to_address(priv, magicbyte=0):
294 | return pubkey_to_address(privkey_to_pubkey(priv), magicbyte)
295 |
296 |
297 | privtoaddr = privkey_to_address
298 |
299 |
300 | def neg_pubkey(pubkey):
301 | f = get_pubkey_format(pubkey)
302 | pubkey = decode_pubkey(pubkey, f)
303 | return encode_pubkey((pubkey[0], (P - pubkey[1]) % P), f)
304 |
305 |
306 | def neg_privkey(privkey):
307 | f = get_privkey_format(privkey)
308 | privkey = decode_privkey(privkey, f)
309 | return encode_privkey((N - privkey) % N, f)
310 |
311 |
312 | def subtract_pubkeys(p1, p2):
313 | f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2)
314 | k2 = decode_pubkey(p2, f2)
315 | return encode_pubkey(
316 | fast_add(
317 | decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1)
318 |
319 |
320 | # Hashes
321 |
322 |
323 | def bin_hash160(string):
324 | intermed = hashlib.sha256(string).digest()
325 | digest = ''
326 | digest = hashlib.new('ripemd160', intermed).digest()
327 | return digest
328 |
329 |
330 | def hash160(string):
331 | return safe_hexlify(bin_hash160(string))
332 |
333 |
334 | def bin_sha256(string):
335 | binary_data = string if isinstance(string, bytes) else bytes(string,
336 | 'utf-8')
337 | return hashlib.sha256(binary_data).digest()
338 |
339 |
340 | def sha256(string):
341 | return bytes_to_hex_string(bin_sha256(string))
342 |
343 |
344 | def bin_ripemd160(string):
345 | digest = hashlib.new('ripemd160', string).digest()
346 | return digest
347 |
348 |
349 | def ripemd160(string):
350 | return safe_hexlify(bin_ripemd160(string))
351 |
352 |
353 | def bin_dbl_sha256(s):
354 | bytes_to_hash = from_string_to_bytes(s)
355 | return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
356 |
357 |
358 | def dbl_sha256(string):
359 | return safe_hexlify(bin_dbl_sha256(string))
360 |
361 |
362 | def bin_slowsha(string):
363 | string = from_string_to_bytes(string)
364 | orig_input = string
365 | for i in range(100000):
366 | string = hashlib.sha256(string + orig_input).digest()
367 | return string
368 |
369 |
370 | def slowsha(string):
371 | return safe_hexlify(bin_slowsha(string))
372 |
373 |
374 | def hash_to_int(x):
375 | if len(x) in [40, 64]:
376 | return decode(x, 16)
377 | return decode(x, 256)
378 |
379 |
380 | def num_to_var_int(x):
381 | x = int(x)
382 | if x < 253: return from_int_to_byte(x)
383 | elif x < 65536: return from_int_to_byte(253) + encode(x, 256, 2)[::-1]
384 | elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1]
385 | else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1]
386 |
387 |
388 | # WTF, Electrum?
389 | def electrum_sig_hash(message):
390 | padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(
391 | message)) + from_string_to_bytes(message)
392 | return bin_dbl_sha256(padded)
393 |
394 | # Encodings
395 |
396 | def b58check_to_bin(inp):
397 | leadingzbytes = len(re.match('^1*', inp).group(0))
398 | data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
399 | assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
400 | return data[1:-4]
401 |
402 |
403 | def get_version_byte(inp):
404 | leadingzbytes = len(re.match('^1*', inp).group(0))
405 | data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
406 | assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
407 | return ord(data[0])
408 |
409 |
410 | def hex_to_b58check(inp, magicbyte=0):
411 | return bin_to_b58check(binascii.unhexlify(inp), magicbyte)
412 |
413 |
414 | def b58check_to_hex(inp):
415 | return safe_hexlify(b58check_to_bin(inp))
416 |
417 |
418 | def pubkey_to_address(pubkey, magicbyte=0):
419 | if isinstance(pubkey, (list, tuple)):
420 | pubkey = encode_pubkey(pubkey, 'bin')
421 | if len(pubkey) in [66, 130]:
422 | return bin_to_b58check(
423 | bin_hash160(binascii.unhexlify(pubkey)), magicbyte)
424 | return bin_to_b58check(bin_hash160(pubkey), magicbyte)
425 |
426 |
427 | pubtoaddr = pubkey_to_address
428 |
429 | # EDCSA
430 |
431 |
432 | def encode_sig(v, r, s):
433 | vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256)
434 |
435 | result = base64.b64encode(vb + b'\x00' * (32 - len(rb)) + rb + b'\x00' * (
436 | 32 - len(sb)) + sb)
437 | return result if is_python2 else str(result, 'utf-8')
438 |
439 |
440 | def decode_sig(sig):
441 | bytez = base64.b64decode(sig)
442 | return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(
443 | bytez[33:], 256)
444 |
445 | # https://tools.ietf.org/html/rfc6979#section-3.2
446 |
447 |
448 | def deterministic_generate_k(msghash, priv):
449 | v = b'\x01' * 32
450 | k = b'\x00' * 32
451 | priv = encode_privkey(priv, 'bin')
452 | msghash = encode(hash_to_int(msghash), 256, 32)
453 | k = hmac.new(k, v + b'\x00' + priv + msghash, hashlib.sha256).digest()
454 | v = hmac.new(k, v, hashlib.sha256).digest()
455 | k = hmac.new(k, v + b'\x01' + priv + msghash, hashlib.sha256).digest()
456 | v = hmac.new(k, v, hashlib.sha256).digest()
457 | return decode(hmac.new(k, v, hashlib.sha256).digest(), 256)
458 |
459 |
460 | def ecdsa_raw_sign(msghash, priv):
461 |
462 | z = hash_to_int(msghash)
463 | k = deterministic_generate_k(msghash, priv)
464 |
465 | r, y = fast_multiply(G, k)
466 | s = inv(k, N) * (z + r * decode_privkey(priv)) % N
467 |
468 | return 27 + (y % 2), r, s
469 |
470 |
471 | def ecdsa_sign(msg, priv):
472 | return encode_sig(*ecdsa_raw_sign(electrum_sig_hash(msg), priv))
473 |
474 | def ecdsa_raw_verify(msghash, vrs, pub):
475 | v, r, s = vrs
476 |
477 | w = inv(s, N)
478 | z = hash_to_int(msghash)
479 |
480 | u1, u2 = z * w % N, r * w % N
481 | x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2))
482 |
483 | return r == x
484 |
485 | def ecdsa_verify(msg, sig, pub):
486 | return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub)
487 |
488 | def estimate_tx_size(ins, outs, txtype='p2pkh'):
489 | '''Estimate transaction size.
490 | Assuming p2pkh:
491 | out: 8+1+3+2+20=34, in: 1+32+4+1+1+~73+1+1+33=147,
492 | ver:4,seq:4, +2 (len in,out)
493 | total ~= 34*len_out + 147*len_in + 10 (sig sizes vary slightly)
494 | '''
495 | if txtype=='p2pkh':
496 | return 10 + ins*147 +34*outs
497 | else:
498 | raise NotImplementedError("Non p2pkh transaction size estimation not"+
499 | "yet implemented")
500 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/py2specials.py:
--------------------------------------------------------------------------------
1 | import sys, re
2 | import binascii
3 | import os
4 | import hashlib
5 |
6 | if sys.version_info.major == 2:
7 | string_types = (str, unicode)
8 | string_or_bytes_types = string_types
9 | int_types = (int, float, long)
10 |
11 | # Base switching
12 | code_strings = {
13 | 2: '01',
14 | 10: '0123456789',
15 | 16: '0123456789abcdef',
16 | 32: 'abcdefghijklmnopqrstuvwxyz234567',
17 | 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
18 | 256: ''.join([chr(x) for x in range(256)])
19 | }
20 |
21 | def bin_dbl_sha256(s):
22 | bytes_to_hash = from_string_to_bytes(s)
23 | return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
24 |
25 | def lpad(msg, symbol, length):
26 | if len(msg) >= length:
27 | return msg
28 | return symbol * (length - len(msg)) + msg
29 |
30 | def get_code_string(base):
31 | if base in code_strings:
32 | return code_strings[base]
33 | else:
34 | raise ValueError("Invalid base!")
35 |
36 | def changebase(string, frm, to, minlen=0):
37 | if frm == to:
38 | return lpad(string, get_code_string(frm)[0], minlen)
39 | return encode(decode(string, frm), to, minlen)
40 |
41 | def bin_to_b58check(inp, magicbyte=0):
42 | inp_fmtd = chr(int(magicbyte)) + inp
43 | leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0))
44 | checksum = bin_dbl_sha256(inp_fmtd)[:4]
45 | return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58)
46 |
47 | def bytes_to_hex_string(b):
48 | return b.encode('hex')
49 |
50 | def safe_from_hex(s):
51 | return s.decode('hex')
52 |
53 | def from_int_to_byte(a):
54 | return chr(a)
55 |
56 | def from_byte_to_int(a):
57 | return ord(a)
58 |
59 | def from_string_to_bytes(a):
60 | return a
61 |
62 | def safe_hexlify(a):
63 | return binascii.hexlify(a)
64 |
65 | def encode(val, base, minlen=0):
66 | base, minlen = int(base), int(minlen)
67 | code_string = get_code_string(base)
68 | result = ""
69 | while val > 0:
70 | result = code_string[val % base] + result
71 | val //= base
72 | return code_string[0] * max(minlen - len(result), 0) + result
73 |
74 | def decode(string, base):
75 | base = int(base)
76 | code_string = get_code_string(base)
77 | result = 0
78 | if base == 16:
79 | string = string.lower()
80 | while len(string) > 0:
81 | result *= base
82 | result += code_string.find(string[0])
83 | string = string[1:]
84 | return result
85 |
86 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/py3specials.py:
--------------------------------------------------------------------------------
1 | import sys, os
2 | import binascii
3 | import hashlib
4 |
5 | if sys.version_info.major == 3:
6 | string_types = (str)
7 | string_or_bytes_types = (str, bytes)
8 | int_types = (int, float)
9 | # Base switching
10 | code_strings = {
11 | 2: '01',
12 | 10: '0123456789',
13 | 16: '0123456789abcdef',
14 | 32: 'abcdefghijklmnopqrstuvwxyz234567',
15 | 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
16 | 256: ''.join([chr(x) for x in range(256)])
17 | }
18 |
19 | def bin_dbl_sha256(s):
20 | bytes_to_hash = from_string_to_bytes(s)
21 | return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
22 |
23 | def lpad(msg, symbol, length):
24 | if len(msg) >= length:
25 | return msg
26 | return symbol * (length - len(msg)) + msg
27 |
28 | def get_code_string(base):
29 | if base in code_strings:
30 | return code_strings[base]
31 | else:
32 | raise ValueError("Invalid base!")
33 |
34 | def changebase(string, frm, to, minlen=0):
35 | if frm == to:
36 | return lpad(string, get_code_string(frm)[0], minlen)
37 | return encode(decode(string, frm), to, minlen)
38 |
39 | def bin_to_b58check(inp, magicbyte=0):
40 | inp_fmtd = from_int_to_byte(int(magicbyte)) + inp
41 |
42 | leadingzbytes = 0
43 | for x in inp_fmtd:
44 | if x != 0:
45 | break
46 | leadingzbytes += 1
47 |
48 | checksum = bin_dbl_sha256(inp_fmtd)[:4]
49 | return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58)
50 |
51 | def bytes_to_hex_string(b):
52 | if isinstance(b, str):
53 | return b
54 |
55 | return ''.join('{:02x}'.format(y) for y in b)
56 |
57 | def safe_from_hex(s):
58 | return bytes.fromhex(s)
59 |
60 | def from_int_to_byte(a):
61 | return bytes([a])
62 |
63 | def from_byte_to_int(a):
64 | return a
65 |
66 | def from_string_to_bytes(a):
67 | return a if isinstance(a, bytes) else bytes(a, 'utf-8')
68 |
69 | def safe_hexlify(a):
70 | return str(binascii.hexlify(a), 'utf-8')
71 |
72 | def encode(val, base, minlen=0):
73 | base, minlen = int(base), int(minlen)
74 | code_string = get_code_string(base)
75 | result_bytes = bytes()
76 | while val > 0:
77 | curcode = code_string[val % base]
78 | result_bytes = bytes([ord(curcode)]) + result_bytes
79 | val //= base
80 |
81 | pad_size = minlen - len(result_bytes)
82 |
83 | padding_element = b'\x00' if base == 256 else b'1' \
84 | if base == 58 else b'0'
85 | if (pad_size > 0):
86 | result_bytes = padding_element * pad_size + result_bytes
87 |
88 | result_string = ''.join([chr(y) for y in result_bytes])
89 | result = result_bytes if base == 256 else result_string
90 |
91 | return result
92 |
93 | def decode(string, base):
94 | if base == 256 and isinstance(string, str):
95 | string = bytes(bytearray.fromhex(string))
96 | base = int(base)
97 | code_string = get_code_string(base)
98 | result = 0
99 | if base == 256:
100 |
101 | def extract(d, cs):
102 | return d
103 | else:
104 |
105 | def extract(d, cs):
106 | return cs.find(d if isinstance(d, str) else chr(d))
107 |
108 | if base == 16:
109 | string = string.lower()
110 | while len(string) > 0:
111 | result *= base
112 | result += extract(string[0], code_string)
113 | string = string[1:]
114 | return result
115 |
116 | def from_int_representation_to_bytes(a):
117 | return bytes(str(a), 'utf-8')
118 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/secp256k1_deterministic.py:
--------------------------------------------------------------------------------
1 | from electrumpersonalserver.bitcoin.secp256k1_main import *
2 | import hmac
3 | import hashlib
4 | from binascii import hexlify
5 |
6 | # Below code ASSUMES binary inputs and compressed pubkeys
7 | MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
8 | MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
9 | TESTNET_PRIVATE = b'\x04\x35\x83\x94'
10 | TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
11 | PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE]
12 | PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC]
13 |
14 | # BIP32 child key derivation
15 |
16 | def raw_bip32_ckd(rawtuple, i):
17 | vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple
18 | i = int(i)
19 |
20 | if vbytes in PRIVATE:
21 | priv = key
22 | pub = privtopub(key, False)
23 | else:
24 | pub = key
25 |
26 | if i >= 2**31:
27 | if vbytes in PUBLIC:
28 | raise Exception("Can't do private derivation on public key!")
29 | I = hmac.new(chaincode, b'\x00' + priv[:32] + encode(i, 256, 4),
30 | hashlib.sha512).digest()
31 | else:
32 | I = hmac.new(chaincode, pub + encode(i, 256, 4),
33 | hashlib.sha512).digest()
34 |
35 | if vbytes in PRIVATE:
36 | newkey = add_privkeys(I[:32] + B'\x01', priv, False)
37 | fingerprint = bin_hash160(privtopub(key, False))[:4]
38 | if vbytes in PUBLIC:
39 | newkey = add_pubkeys([privtopub(I[:32] + '\x01', False), key], False)
40 | fingerprint = bin_hash160(key)[:4]
41 |
42 | return (vbytes, depth + 1, fingerprint, i, I[32:], newkey)
43 |
44 | def bip32_serialize(rawtuple):
45 | vbytes, depth, fingerprint, i, chaincode, key = rawtuple
46 | i = encode(i, 256, 4)
47 | chaincode = encode(hash_to_int(chaincode), 256, 32)
48 | keydata = b'\x00' + key[:-1] if vbytes in PRIVATE else key
49 | bindata = vbytes + from_int_to_byte(
50 | depth % 256) + fingerprint + i + chaincode + keydata
51 | return changebase(bindata + bin_dbl_sha256(bindata)[:4], 256, 58)
52 |
53 | def bip32_deserialize(data):
54 | dbin = changebase(data, 58, 256)
55 | if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]:
56 | raise Exception("Invalid checksum")
57 | vbytes = dbin[0:4]
58 | depth = from_byte_to_int(dbin[4])
59 | fingerprint = dbin[5:9]
60 | i = decode(dbin[9:13], 256)
61 | chaincode = dbin[13:45]
62 | key = dbin[46:78] + b'\x01' if vbytes in PRIVATE else dbin[45:78]
63 | return (vbytes, depth, fingerprint, i, chaincode, key)
64 |
65 | def raw_bip32_privtopub(rawtuple):
66 | vbytes, depth, fingerprint, i, chaincode, key = rawtuple
67 | newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
68 | return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key, False))
69 |
70 | def bip32_privtopub(data):
71 | return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data)))
72 |
73 | def bip32_ckd(data, i):
74 | return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i))
75 |
76 | def bip32_master_key(seed, vbytes=MAINNET_PRIVATE):
77 | I = hmac.new(
78 | from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest()
79 | return bip32_serialize((vbytes, 0, b'\x00' * 4, 0, I[32:], I[:32] + b'\x01'
80 | ))
81 |
82 | def bip32_extract_key(data):
83 | return safe_hexlify(bip32_deserialize(data)[-1])
84 |
85 | def bip32_descend(*args):
86 | if len(args) == 2:
87 | key, path = args
88 | else:
89 | key, path = args[0], map(int, args[1:])
90 | for p in path:
91 | key = bip32_ckd(key, p)
92 | return bip32_extract_key(key)
93 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/secp256k1_main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | from .py2specials import *
3 | from .py3specials import *
4 | import binascii
5 | import hashlib
6 | import re
7 | import sys
8 | import os
9 | import base64
10 | import time
11 | import random
12 | import hmac
13 | import secp256k1
14 |
15 |
16 | def privkey_to_address(priv, from_hex=True, magicbyte=0):
17 | return pubkey_to_address(privkey_to_pubkey(priv, from_hex), magicbyte)
18 |
19 | privtoaddr = privkey_to_address
20 |
21 | # Hashes
22 | def bin_hash160(string):
23 | intermed = hashlib.sha256(string).digest()
24 | return hashlib.new('ripemd160', intermed).digest()
25 |
26 | def hash160(string):
27 | return safe_hexlify(bin_hash160(string))
28 |
29 | def bin_sha256(string):
30 | binary_data = string if isinstance(string, bytes) else bytes(string,
31 | 'utf-8')
32 | return hashlib.sha256(binary_data).digest()
33 |
34 | def sha256(string):
35 | return bytes_to_hex_string(bin_sha256(string))
36 |
37 | def bin_dbl_sha256(s):
38 | bytes_to_hash = from_string_to_bytes(s)
39 | return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest()
40 |
41 | def dbl_sha256(string):
42 | return safe_hexlify(bin_dbl_sha256(string))
43 |
44 | def hash_to_int(x):
45 | if len(x) in [40, 64]:
46 | return decode(x, 16)
47 | return decode(x, 256)
48 |
49 | def num_to_var_int(x):
50 | x = int(x)
51 | if x < 253: return from_int_to_byte(x)
52 | elif x < 65536: return from_int_to_byte(253) + encode(x, 256, 2)[::-1]
53 | elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1]
54 | else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1]
55 |
56 | # WTF, Electrum?
57 | def electrum_sig_hash(message):
58 | padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(
59 | message)) + from_string_to_bytes(message)
60 | return bin_dbl_sha256(padded)
61 |
62 | # Encodings
63 | def b58check_to_bin(inp):
64 | leadingzbytes = len(re.match('^1*', inp).group(0))
65 | data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
66 | assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
67 | return data[1:-4]
68 |
69 | def get_version_byte(inp):
70 | leadingzbytes = len(re.match('^1*', inp).group(0))
71 | data = b'\x00' * leadingzbytes + changebase(inp, 58, 256)
72 | assert bin_dbl_sha256(data[:-4])[:4] == data[-4:]
73 | return ord(data[0])
74 |
75 | def hex_to_b58check(inp, magicbyte=0):
76 | return bin_to_b58check(binascii.unhexlify(inp), magicbyte)
77 |
78 | def b58check_to_hex(inp):
79 | return safe_hexlify(b58check_to_bin(inp))
80 |
81 | def pubkey_to_address(pubkey, magicbyte=0):
82 | if len(pubkey) in [66, 130]:
83 | return bin_to_b58check(
84 | bin_hash160(binascii.unhexlify(pubkey)), magicbyte)
85 | return bin_to_b58check(bin_hash160(pubkey), magicbyte)
86 |
87 | pubtoaddr = pubkey_to_address
88 |
89 | def wif_compressed_privkey(priv, vbyte=0):
90 | """Convert privkey in hex compressed to WIF compressed
91 | """
92 | if len(priv) != 66:
93 | raise Exception("Wrong length of compressed private key")
94 | if priv[-2:] != '01':
95 | raise Exception("Private key has wrong compression byte")
96 | return bin_to_b58check(binascii.unhexlify(priv), 128 + int(vbyte))
97 |
98 |
99 | def from_wif_privkey(wif_priv, compressed=True, vbyte=0):
100 | """Convert WIF compressed privkey to hex compressed.
101 | Caller specifies the network version byte (0 for mainnet, 0x6f
102 | for testnet) that the key should correspond to; if there is
103 | a mismatch an error is thrown. WIF encoding uses 128+ this number.
104 | """
105 | bin_key = b58check_to_bin(wif_priv)
106 | claimed_version_byte = get_version_byte(wif_priv)
107 | if not 128+vbyte == claimed_version_byte:
108 | raise Exception(
109 | "WIF key version byte is wrong network (mainnet/testnet?)")
110 | if compressed and not len(bin_key) == 33:
111 | raise Exception("Compressed private key is not 33 bytes")
112 | if compressed and not bin_key[-1] == '\x01':
113 | raise Exception("Private key has incorrect compression byte")
114 | return safe_hexlify(bin_key)
115 |
116 | def ecdsa_sign(msg, priv, usehex=True):
117 | #Compatibility issue: old bots will be confused
118 | #by different msg hashing algo; need to keep electrum_sig_hash, temporarily.
119 | hashed_msg = electrum_sig_hash(msg)
120 | if usehex:
121 | #arguments to raw sign must be consistently hex or bin
122 | hashed_msg = binascii.hexlify(hashed_msg)
123 | dersig = ecdsa_raw_sign(hashed_msg, priv, usehex, rawmsg=True)
124 | #see comments to legacy* functions
125 | #also, note those functions only handles binary, not hex
126 | if usehex:
127 | dersig = binascii.unhexlify(dersig)
128 | sig = legacy_ecdsa_sign_convert(dersig)
129 | return base64.b64encode(sig)
130 |
131 | def ecdsa_verify(msg, sig, pub, usehex=True):
132 | #See note to ecdsa_sign
133 | hashed_msg = electrum_sig_hash(msg)
134 | sig = base64.b64decode(sig)
135 | #see comments to legacy* functions
136 | sig = legacy_ecdsa_verify_convert(sig)
137 | if usehex:
138 | #arguments to raw_verify must be consistently hex or bin
139 | hashed_msg = binascii.hexlify(hashed_msg)
140 | sig = binascii.hexlify(sig)
141 | return ecdsa_raw_verify(hashed_msg, pub, sig, usehex, rawmsg=True)
142 |
143 | #A sadly necessary hack until all joinmarket bots are running secp256k1 code.
144 | #pybitcointools *message* signatures (not transaction signatures) used an old signature
145 | #format, basically: [27+y%2] || 32 byte r || 32 byte s,
146 | #instead of DER. These two functions translate the new version into the old so that
147 | #counterparty bots can verify successfully.
148 | def legacy_ecdsa_sign_convert(dersig):
149 | #note there is no sanity checking of DER format (e.g. leading length byte)
150 | dersig = dersig[2:] #e.g. 3045
151 | rlen = ord(dersig[1]) #ignore leading 02
152 | #length of r and s: ALWAYS <=33, USUALLY >=32 but can be shorter
153 | if rlen > 33:
154 | raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
155 | dersig))
156 | if dersig[2] == '\x00':
157 | r = dersig[3:2 + rlen]
158 | ssig = dersig[2 + rlen:]
159 | else:
160 | r = dersig[2:2 + rlen]
161 | ssig = dersig[2 + rlen:]
162 |
163 | slen = ord(ssig[1]) #ignore leading 02
164 | if slen > 33:
165 | raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
166 | dersig))
167 | if len(ssig) != 2 + slen:
168 | raise Exception("Incorrectly formatted DER sig:" + binascii.hexlify(
169 | dersig))
170 | if ssig[2] == '\x00':
171 | s = ssig[3:2 + slen]
172 | else:
173 | s = ssig[2:2 + slen]
174 |
175 | #the legacy version requires padding of r and s to 32 bytes with leading zeros
176 | r = '\x00' * (32 - len(r)) + r
177 | s = '\x00' * (32 - len(s)) + s
178 |
179 | #note: in the original pybitcointools implementation,
180 | #verification ignored the leading byte (it's only needed for pubkey recovery)
181 | #so we just ignore parity here.
182 | return chr(27) + r + s
183 |
184 | def legacy_ecdsa_verify_convert(sig):
185 | sig = sig[1:] #ignore parity byte
186 | r, s = sig[:32], sig[32:]
187 | if not len(s) == 32:
188 | #signature is invalid.
189 | return False
190 | #legacy code can produce high S. Need to reintroduce N ::cry::
191 | N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
192 | s_int = decode(s, 256)
193 | # note // is integer division operator in both 2.7 and 3
194 | s_int = N - s_int if s_int > N // 2 else s_int #enforce low S.
195 |
196 | #on re-encoding, don't use the minlen parameter, because
197 | #DER does not used fixed (32 byte) length values, so we
198 | #don't prepend zero bytes to shorter numbers.
199 | s = encode(s_int, 256)
200 |
201 | #as above, remove any front zero padding from r.
202 | r = encode(decode(r, 256), 256)
203 |
204 | #canonicalize r and s
205 | r, s = ['\x00' + x if ord(x[0]) > 127 else x for x in [r, s]]
206 | rlen = chr(len(r))
207 | slen = chr(len(s))
208 | total_len = 2 + len(r) + 2 + len(s)
209 | return '\x30' + chr(total_len) + '\x02' + rlen + r + '\x02' + slen + s
210 |
211 | #Use secp256k1 to handle all EC and ECDSA operations.
212 | #Data types: only hex and binary.
213 | #Compressed and uncompressed private and public keys.
214 | def hexbin(func):
215 | '''To enable each function to 'speak' either hex or binary,
216 | requires that the decorated function's final positional argument
217 | is a boolean flag, True for hex and False for binary.
218 | '''
219 |
220 | def func_wrapper(*args, **kwargs):
221 | if args[-1]:
222 | newargs = []
223 | for arg in args[:-1]:
224 | if isinstance(arg, (list, tuple)):
225 | newargs += [[x.decode('hex') for x in arg]]
226 | else:
227 | newargs += [arg.decode('hex')]
228 | newargs += [False]
229 | returnval = func(*newargs, **kwargs)
230 | if isinstance(returnval, bool):
231 | return returnval
232 | else:
233 | return binascii.hexlify(returnval)
234 | else:
235 | return func(*args, **kwargs)
236 |
237 | return func_wrapper
238 |
239 | def read_privkey(priv):
240 | if len(priv) == 33:
241 | if priv[-1] == '\x01':
242 | compressed = True
243 | else:
244 | raise Exception("Invalid private key")
245 | elif len(priv) == 32:
246 | compressed = False
247 | else:
248 | raise Exception("Invalid private key")
249 | return (compressed, priv[:32])
250 |
251 | @hexbin
252 | def privkey_to_pubkey_inner(priv, usehex):
253 | '''Take 32/33 byte raw private key as input.
254 | If 32 bytes, return compressed (33 byte) raw public key.
255 | If 33 bytes, read the final byte as compression flag,
256 | and return compressed/uncompressed public key as appropriate.'''
257 | compressed, priv = read_privkey(priv)
258 | #secp256k1 checks for validity of key value.
259 | newpriv = secp256k1.PrivateKey(privkey=priv)
260 | return newpriv.pubkey.serialize(compressed=compressed)
261 |
262 | def privkey_to_pubkey(priv, usehex=True):
263 | '''To avoid changing the interface from the legacy system,
264 | allow an *optional* hex argument here (called differently from
265 | maker/taker code to how it's called in bip32 code), then
266 | pass to the standard hexbin decorator under the hood.
267 | '''
268 | return privkey_to_pubkey_inner(priv, usehex)
269 |
270 | privtopub = privkey_to_pubkey
271 |
272 | @hexbin
273 | def multiply(s, pub, usehex, rawpub=True):
274 | '''Input binary compressed pubkey P(33 bytes)
275 | and scalar s(32 bytes), return s*P.
276 | The return value is a binary compressed public key.
277 | Note that the called function does the type checking
278 | of the scalar s.
279 | ('raw' options passed in)
280 | '''
281 | newpub = secp256k1.PublicKey(pub, raw=rawpub)
282 | res = newpub.tweak_mul(s)
283 | return res.serialize()
284 |
285 | @hexbin
286 | def add_pubkeys(pubkeys, usehex):
287 | '''Input a list of binary compressed pubkeys
288 | and return their sum as a binary compressed pubkey.'''
289 | r = secp256k1.PublicKey() #dummy holding object
290 | pubkey_list = [secp256k1.PublicKey(x,
291 | raw=True).public_key for x in pubkeys]
292 | r.combine(pubkey_list)
293 | return r.serialize()
294 |
295 | @hexbin
296 | def add_privkeys(priv1, priv2, usehex):
297 | '''Add privkey 1 to privkey 2.
298 | Input keys must be in binary either compressed or not.
299 | Returned key will have the same compression state.
300 | Error if compression state of both input keys is not the same.'''
301 | y, z = [read_privkey(x) for x in [priv1, priv2]]
302 | if y[0] != z[0]:
303 | raise Exception("cannot add privkeys, mixed compression formats")
304 | else:
305 | compressed = y[0]
306 | newpriv1, newpriv2 = (y[1], z[1])
307 | p1 = secp256k1.PrivateKey(newpriv1, raw=True)
308 | res = p1.tweak_add(newpriv2)
309 | if compressed:
310 | res += '\x01'
311 | return res
312 |
313 | @hexbin
314 | def ecdsa_raw_sign(msg,
315 | priv,
316 | usehex,
317 | rawpriv=True,
318 | rawmsg=False,
319 | usenonce=None):
320 | '''Take the binary message msg and sign it with the private key
321 | priv.
322 | By default priv is just a 32 byte string, if rawpriv is false
323 | it is assumed to be DER encoded.
324 | If rawmsg is True, no sha256 hash is applied to msg before signing.
325 | In this case, msg must be a precalculated hash (256 bit).
326 | If rawmsg is False, the secp256k1 lib will hash the message as part
327 | of the ECDSA-SHA256 signing algo.
328 | If usenonce is not None, its value is passed to the secp256k1 library
329 | sign() function as the ndata value, which is then used in conjunction
330 | with a custom nonce generating function, such that the nonce used in the ECDSA
331 | sign algorithm is exactly that value (ndata there, usenonce here). 32 bytes.
332 | Return value: the calculated signature.'''
333 | if rawmsg and len(msg) != 32:
334 | raise Exception("Invalid hash input to ECDSA raw sign.")
335 | if rawpriv:
336 | compressed, p = read_privkey(priv)
337 | newpriv = secp256k1.PrivateKey(p, raw=True)
338 | else:
339 | newpriv = secp256k1.PrivateKey(priv, raw=False)
340 | if usenonce and len(usenonce) != 32:
341 | raise ValueError("Invalid nonce passed to ecdsa_sign: " + str(usenonce))
342 |
343 | sig = newpriv.ecdsa_sign(msg, raw=rawmsg)
344 | return newpriv.ecdsa_serialize(sig)
345 |
346 | @hexbin
347 | def ecdsa_raw_verify(msg, pub, sig, usehex, rawmsg=False):
348 | '''Take the binary message msg and binary signature sig,
349 | and verify it against the pubkey pub.
350 | If rawmsg is True, no sha256 hash is applied to msg before verifying.
351 | In this case, msg must be a precalculated hash (256 bit).
352 | If rawmsg is False, the secp256k1 lib will hash the message as part
353 | of the ECDSA-SHA256 verification algo.
354 | Return value: True if the signature is valid for this pubkey, False
355 | otherwise. '''
356 | if rawmsg and len(msg) != 32:
357 | raise Exception("Invalid hash input to ECDSA raw sign.")
358 | newpub = secp256k1.PublicKey(pubkey=pub, raw=True)
359 | sigobj = newpub.ecdsa_deserialize(sig)
360 | return newpub.ecdsa_verify(msg, sigobj, raw=rawmsg)
361 |
362 | def estimate_tx_size(ins, outs, txtype='p2pkh'):
363 | '''Estimate transaction size.
364 | Assuming p2pkh:
365 | out: 8+1+3+2+20=34, in: 1+32+4+1+1+~73+1+1+33=147,
366 | ver:4,seq:4, +2 (len in,out)
367 | total ~= 34*len_out + 147*len_in + 10 (sig sizes vary slightly)
368 | '''
369 | if txtype == 'p2pkh':
370 | return 10 + ins * 147 + 34 * outs
371 | else:
372 | raise NotImplementedError("Non p2pkh transaction size estimation not" +
373 | "yet implemented")
374 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/secp256k1_transaction.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | import binascii, re, json, copy, sys
3 | from electrumpersonalserver.bitcoin.secp256k1_main import *
4 | from _functools import reduce
5 | import os
6 |
7 | is_python2 = sys.version_info.major == 2
8 |
9 | ### Hex to bin converter and vice versa for objects
10 | def json_is_base(obj, base):
11 | if not is_python2 and isinstance(obj, bytes):
12 | return False
13 |
14 | alpha = get_code_string(base)
15 | if isinstance(obj, string_types):
16 | for i in range(len(obj)):
17 | if alpha.find(obj[i]) == -1:
18 | return False
19 | return True
20 | elif isinstance(obj, int_types) or obj is None:
21 | return True
22 | elif isinstance(obj, list):
23 | for i in range(len(obj)):
24 | if not json_is_base(obj[i], base):
25 | return False
26 | return True
27 | else:
28 | for x in obj:
29 | if not json_is_base(obj[x], base):
30 | return False
31 | return True
32 |
33 |
34 | def json_changebase(obj, changer):
35 | if isinstance(obj, string_or_bytes_types):
36 | return changer(obj)
37 | elif isinstance(obj, int_types) or obj is None:
38 | return obj
39 | elif isinstance(obj, list):
40 | return [json_changebase(x, changer) for x in obj]
41 | return dict((x, json_changebase(obj[x], changer)) for x in obj)
42 |
43 | # Transaction serialization and deserialization
44 |
45 |
46 | def deserialize(tx):
47 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
48 | #tx = bytes(bytearray.fromhex(tx))
49 | return json_changebase(
50 | deserialize(binascii.unhexlify(tx)), lambda x: safe_hexlify(x))
51 | # http://stackoverflow.com/questions/4851463/python-closure-write-to-variable-in-parent-scope
52 | # Python's scoping rules are demented, requiring me to make pos an object
53 | # so that it is call-by-reference
54 | pos = [0]
55 |
56 | def read_as_int(bytez):
57 | pos[0] += bytez
58 | return decode(tx[pos[0] - bytez:pos[0]][::-1], 256)
59 |
60 | def read_var_int():
61 | pos[0] += 1
62 |
63 | val = from_byte_to_int(tx[pos[0] - 1])
64 | if val < 253:
65 | return val
66 | return read_as_int(pow(2, val - 252))
67 |
68 | def read_bytes(bytez):
69 | pos[0] += bytez
70 | return tx[pos[0] - bytez:pos[0]]
71 |
72 | def read_var_string():
73 | size = read_var_int()
74 | return read_bytes(size)
75 |
76 | obj = {"ins": [], "outs": []}
77 | obj["version"] = read_as_int(4)
78 | ins = read_var_int()
79 | for i in range(ins):
80 | obj["ins"].append({
81 | "outpoint": {
82 | "hash": read_bytes(32)[::-1],
83 | "index": read_as_int(4)
84 | },
85 | "script": read_var_string(),
86 | "sequence": read_as_int(4)
87 | })
88 | outs = read_var_int()
89 | for i in range(outs):
90 | obj["outs"].append({
91 | "value": read_as_int(8),
92 | "script": read_var_string()
93 | })
94 | obj["locktime"] = read_as_int(4)
95 | return obj
96 |
97 |
98 | def serialize(txobj):
99 | #if isinstance(txobj, bytes):
100 | # txobj = bytes_to_hex_string(txobj)
101 | o = []
102 | if json_is_base(txobj, 16):
103 | json_changedbase = json_changebase(txobj,
104 | lambda x: binascii.unhexlify(x))
105 | hexlified = safe_hexlify(serialize(json_changedbase))
106 | return hexlified
107 | o.append(encode(txobj["version"], 256, 4)[::-1])
108 | o.append(num_to_var_int(len(txobj["ins"])))
109 | for inp in txobj["ins"]:
110 | o.append(inp["outpoint"]["hash"][::-1])
111 | o.append(encode(inp["outpoint"]["index"], 256, 4)[::-1])
112 | o.append(num_to_var_int(len(inp["script"])) + (inp["script"] if inp[
113 | "script"] or is_python2 else bytes()))
114 | o.append(encode(inp["sequence"], 256, 4)[::-1])
115 | o.append(num_to_var_int(len(txobj["outs"])))
116 | for out in txobj["outs"]:
117 | o.append(encode(out["value"], 256, 8)[::-1])
118 | o.append(num_to_var_int(len(out["script"])) + out["script"])
119 | o.append(encode(txobj["locktime"], 256, 4)[::-1])
120 |
121 | return ''.join(o) if is_python2 else reduce(lambda x, y: x + y, o, bytes())
122 |
123 | # Hashing transactions for signing
124 |
125 | SIGHASH_ALL = 1
126 | SIGHASH_NONE = 2
127 | SIGHASH_SINGLE = 3
128 | SIGHASH_ANYONECANPAY = 0x80
129 |
130 | def signature_form(tx, i, script, hashcode=SIGHASH_ALL):
131 | i, hashcode = int(i), int(hashcode)
132 | if isinstance(tx, string_or_bytes_types):
133 | return serialize(signature_form(deserialize(tx), i, script, hashcode))
134 | newtx = copy.deepcopy(tx)
135 | for inp in newtx["ins"]:
136 | inp["script"] = ""
137 | newtx["ins"][i]["script"] = script
138 | if hashcode & 0x1f == SIGHASH_NONE:
139 | newtx["outs"] = []
140 | for j, inp in enumerate(newtx["ins"]):
141 | if j != i:
142 | inp["sequence"] = 0
143 | elif hashcode & 0x1f == SIGHASH_SINGLE:
144 | if len(newtx["ins"]) > len(newtx["outs"]):
145 | raise Exception(
146 | "Transactions with sighash single should have len in <= len out")
147 | newtx["outs"] = newtx["outs"][:i+1]
148 | for out in newtx["outs"][:i]:
149 | out['value'] = 2**64 - 1
150 | out['script'] = ""
151 | for j, inp in enumerate(newtx["ins"]):
152 | if j != i:
153 | inp["sequence"] = 0
154 | if hashcode & SIGHASH_ANYONECANPAY:
155 | newtx["ins"] = [newtx["ins"][i]]
156 | else:
157 | pass
158 | return newtx
159 |
160 | def txhash(tx, hashcode=None):
161 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
162 | tx = changebase(tx, 16, 256)
163 | if hashcode:
164 | return dbl_sha256(from_string_to_bytes(tx) + encode(
165 | int(hashcode), 256, 4)[::-1])
166 | else:
167 | return safe_hexlify(bin_dbl_sha256(tx)[::-1])
168 |
169 |
170 | def bin_txhash(tx, hashcode=None):
171 | return binascii.unhexlify(txhash(tx, hashcode))
172 |
173 |
174 | def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL, usenonce=None):
175 | sig = ecdsa_raw_sign(
176 | txhash(tx, hashcode),
177 | priv,
178 | True,
179 | rawmsg=True,
180 | usenonce=usenonce)
181 | return sig + encode(hashcode, 16, 2)
182 |
183 |
184 | def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL):
185 | return ecdsa_raw_verify(
186 | txhash(tx, hashcode),
187 | pub,
188 | sig[:-2],
189 | True,
190 | rawmsg=True)
191 |
192 | # Scripts
193 |
194 |
195 | def mk_pubkey_script(addr):
196 | # Keep the auxiliary functions around for altcoins' sake
197 | return '76a914' + b58check_to_hex(addr) + '88ac'
198 |
199 |
200 | def mk_scripthash_script(addr):
201 | return 'a914' + b58check_to_hex(addr) + '87'
202 |
203 | # Address representation to output script
204 |
205 |
206 | def address_to_script(addr):
207 | if addr[0] == '3' or addr[0] == '2':
208 | return mk_scripthash_script(addr)
209 | else:
210 | return mk_pubkey_script(addr)
211 |
212 | # Output script to address representation
213 |
214 |
215 | def script_to_address(script, vbyte=0):
216 | if re.match('^[0-9a-fA-F]*#39;, script):
217 | script = binascii.unhexlify(script)
218 | if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(
219 | script) == 25:
220 | return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses
221 | else:
222 | if vbyte in [111, 196]:
223 | # Testnet
224 | scripthash_byte = 196
225 | else:
226 | scripthash_byte = 5
227 | # BIP0016 scripthash addresses
228 | return bin_to_b58check(script[2:-1], scripthash_byte)
229 |
230 |
231 | def p2sh_scriptaddr(script, magicbyte=5):
232 | if re.match('^[0-9a-fA-F]*#39;, script):
233 | script = binascii.unhexlify(script)
234 | return hex_to_b58check(hash160(script), magicbyte)
235 |
236 |
237 | scriptaddr = p2sh_scriptaddr
238 |
239 |
240 | def deserialize_script(script):
241 | if isinstance(script, str) and re.match('^[0-9a-fA-F]*#39;, script):
242 | return json_changebase(
243 | deserialize_script(binascii.unhexlify(script)),
244 | lambda x: safe_hexlify(x))
245 | out, pos = [], 0
246 | while pos < len(script):
247 | code = from_byte_to_int(script[pos])
248 | if code == 0:
249 | out.append(None)
250 | pos += 1
251 | elif code <= 75:
252 | out.append(script[pos + 1:pos + 1 + code])
253 | pos += 1 + code
254 | elif code <= 78:
255 | szsz = pow(2, code - 76)
256 | sz = decode(script[pos + szsz:pos:-1], 256)
257 | out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz])
258 | pos += 1 + szsz + sz
259 | elif code <= 96:
260 | out.append(code - 80)
261 | pos += 1
262 | else:
263 | out.append(code)
264 | pos += 1
265 | return out
266 |
267 |
268 | def serialize_script_unit(unit):
269 | if isinstance(unit, int):
270 | if unit < 16:
271 | return from_int_to_byte(unit + 80)
272 | else:
273 | return bytes([unit])
274 | elif unit is None:
275 | return b'\x00'
276 | else:
277 | if len(unit) <= 75:
278 | return from_int_to_byte(len(unit)) + unit
279 | elif len(unit) < 256:
280 | return from_int_to_byte(76) + from_int_to_byte(len(unit)) + unit
281 | elif len(unit) < 65536:
282 | return from_int_to_byte(77) + encode(len(unit), 256, 2)[::-1] + unit
283 | else:
284 | return from_int_to_byte(78) + encode(len(unit), 256, 4)[::-1] + unit
285 |
286 |
287 | if is_python2:
288 |
289 | def serialize_script(script):
290 | if json_is_base(script, 16):
291 | return binascii.hexlify(serialize_script(json_changebase(
292 | script, lambda x: binascii.unhexlify(x))))
293 | return ''.join(map(serialize_script_unit, script))
294 | else:
295 |
296 | def serialize_script(script):
297 | if json_is_base(script, 16):
298 | return safe_hexlify(serialize_script(json_changebase(
299 | script, lambda x: binascii.unhexlify(x))))
300 |
301 | result = bytes()
302 | for b in map(serialize_script_unit, script):
303 | result += b if isinstance(b, bytes) else bytes(b, 'utf-8')
304 | return result
305 |
306 |
307 | def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k
308 | if isinstance(args[0], list):
309 | pubs, k = args[0], int(args[1])
310 | else:
311 | pubs = list(filter(lambda x: len(str(x)) >= 32, args))
312 | k = int(args[len(pubs)])
313 | return serialize_script([k] + pubs + [len(pubs)]) + 'ae'
314 |
315 | # Signing and verifying
316 |
317 |
318 | def verify_tx_input(tx, i, script, sig, pub):
319 | if re.match('^[0-9a-fA-F]*#39;, tx):
320 | tx = binascii.unhexlify(tx)
321 | if re.match('^[0-9a-fA-F]*#39;, script):
322 | script = binascii.unhexlify(script)
323 | if not re.match('^[0-9a-fA-F]*#39;, sig):
324 | sig = safe_hexlify(sig)
325 | if not re.match('^[0-9a-fA-F]*#39;, pub):
326 | pub = safe_hexlify(pub)
327 | hashcode = decode(sig[-2:], 16)
328 | modtx = signature_form(tx, int(i), script, hashcode)
329 | return ecdsa_tx_verify(modtx, sig, pub, hashcode)
330 |
331 |
332 | def sign(tx, i, priv, hashcode=SIGHASH_ALL, usenonce=None):
333 | i = int(i)
334 | if (not is_python2 and isinstance(re, bytes)) or not re.match(
335 | '^[0-9a-fA-F]*#39;, tx):
336 | return binascii.unhexlify(sign(safe_hexlify(tx), i, priv))
337 | if len(priv) <= 33:
338 | priv = safe_hexlify(priv)
339 | pub = privkey_to_pubkey(priv, True)
340 | address = pubkey_to_address(pub)
341 | signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode)
342 | sig = ecdsa_tx_sign(signing_tx, priv, hashcode, usenonce=usenonce)
343 | txobj = deserialize(tx)
344 | txobj["ins"][i]["script"] = serialize_script([sig, pub])
345 | return serialize(txobj)
346 |
347 |
348 | def signall(tx, priv):
349 | # if priv is a dictionary, assume format is
350 | # { 'txinhash:txinidx' : privkey }
351 | if isinstance(priv, dict):
352 | for e, i in enumerate(deserialize(tx)["ins"]):
353 | k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])]
354 | tx = sign(tx, e, k)
355 | else:
356 | for i in range(len(deserialize(tx)["ins"])):
357 | tx = sign(tx, i, priv)
358 | return tx
359 |
360 |
361 | def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL):
362 | if re.match('^[0-9a-fA-F]*#39;, tx):
363 | tx = binascii.unhexlify(tx)
364 | if re.match('^[0-9a-fA-F]*#39;, script):
365 | script = binascii.unhexlify(script)
366 | modtx = signature_form(tx, i, script, hashcode)
367 | return ecdsa_tx_sign(modtx, pk, hashcode)
368 |
369 |
370 | def apply_multisignatures(*args):
371 | # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n]
372 | tx, i, script = args[0], int(args[1]), args[2]
373 | sigs = args[3] if isinstance(args[3], list) else list(args[3:])
374 |
375 | if isinstance(script, str) and re.match('^[0-9a-fA-F]*#39;, script):
376 | script = binascii.unhexlify(script)
377 | sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs]
378 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
379 | return safe_hexlify(apply_multisignatures(
380 | binascii.unhexlify(tx), i, script, sigs))
381 |
382 | txobj = deserialize(tx)
383 | txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
384 | return serialize(txobj)
385 |
386 |
387 | def is_inp(arg):
388 | return len(arg) > 64 or "output" in arg or "outpoint" in arg
389 |
390 |
391 | def mktx(*args):
392 | # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ...
393 | ins, outs = [], []
394 | for arg in args:
395 | if isinstance(arg, list):
396 | for a in arg:
397 | (ins if is_inp(a) else outs).append(a)
398 | else:
399 | (ins if is_inp(arg) else outs).append(arg)
400 |
401 | txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []}
402 | for i in ins:
403 | if isinstance(i, dict) and "outpoint" in i:
404 | txobj["ins"].append(i)
405 | else:
406 | if isinstance(i, dict) and "output" in i:
407 | i = i["output"]
408 | txobj["ins"].append({
409 | "outpoint": {"hash": i[:64],
410 | "index": int(i[65:])},
411 | "script": "",
412 | "sequence": 4294967295
413 | })
414 | for o in outs:
415 | if isinstance(o, string_or_bytes_types):
416 | addr = o[:o.find(':')]
417 | val = int(o[o.find(':') + 1:])
418 | o = {}
419 | if re.match('^[0-9a-fA-F]*#39;, addr):
420 | o["script"] = addr
421 | else:
422 | o["address"] = addr
423 | o["value"] = val
424 |
425 | outobj = {}
426 | if "address" in o:
427 | outobj["script"] = address_to_script(o["address"])
428 | elif "script" in o:
429 | outobj["script"] = o["script"]
430 | else:
431 | raise Exception("Could not find 'address' or 'script' in output.")
432 | outobj["value"] = o["value"]
433 | txobj["outs"].append(outobj)
434 |
435 | return serialize(txobj)
436 |
437 |
438 | def select(unspent, value):
439 | value = int(value)
440 | high = [u for u in unspent if u["value"] >= value]
441 | high.sort(key=lambda u: u["value"])
442 | low = [u for u in unspent if u["value"] < value]
443 | low.sort(key=lambda u: -u["value"])
444 | if len(high):
445 | return [high[0]]
446 | i, tv = 0, 0
447 | while tv < value and i < len(low):
448 | tv += low[i]["value"]
449 | i += 1
450 | if tv < value:
451 | raise Exception("Not enough funds")
452 | return low[:i]
453 |
--------------------------------------------------------------------------------
/electrumpersonalserver/bitcoin/transaction.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | import binascii, re, json, copy, sys
3 | from electrumpersonalserver.bitcoin.main import *
4 | from _functools import reduce
5 |
6 | ### Hex to bin converter and vice versa for objects
7 |
8 |
9 | def json_is_base(obj, base):
10 | if not is_python2 and isinstance(obj, bytes):
11 | return False
12 |
13 | alpha = get_code_string(base)
14 | if isinstance(obj, string_types):
15 | for i in range(len(obj)):
16 | if alpha.find(obj[i]) == -1:
17 | return False
18 | return True
19 | elif isinstance(obj, int_types) or obj is None:
20 | return True
21 | elif isinstance(obj, list):
22 | for i in range(len(obj)):
23 | if not json_is_base(obj[i], base):
24 | return False
25 | return True
26 | else:
27 | for x in obj:
28 | if not json_is_base(obj[x], base):
29 | return False
30 | return True
31 |
32 |
33 | def json_changebase(obj, changer):
34 | if isinstance(obj, string_or_bytes_types):
35 | return changer(obj)
36 | elif isinstance(obj, int_types) or obj is None:
37 | return obj
38 | elif isinstance(obj, list):
39 | return [json_changebase(x, changer) for x in obj]
40 | return dict((x, json_changebase(obj[x], changer)) for x in obj)
41 |
42 | # Transaction serialization and deserialization
43 |
44 |
45 | def deserialize(tx):
46 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
47 | #tx = bytes(bytearray.fromhex(tx))
48 | return json_changebase(
49 | deserialize(binascii.unhexlify(tx)), lambda x: safe_hexlify(x))
50 | # http://stackoverflow.com/questions/4851463/python-closure-write-to-variable-in-parent-scope
51 | # Python's scoping rules are demented, requiring me to make pos an object
52 | # so that it is call-by-reference
53 | pos = [0]
54 |
55 | def read_as_int(bytez):
56 | pos[0] += bytez
57 | return decode(tx[pos[0] - bytez:pos[0]][::-1], 256)
58 |
59 | def read_var_int():
60 | pos[0] += 1
61 |
62 | val = from_byte_to_int(tx[pos[0] - 1])
63 | if val < 253:
64 | return val
65 | return read_as_int(pow(2, val - 252))
66 |
67 | def read_bytes(bytez):
68 | pos[0] += bytez
69 | return tx[pos[0] - bytez:pos[0]]
70 |
71 | def read_var_string():
72 | size = read_var_int()
73 | return read_bytes(size)
74 |
75 | obj = {"ins": [], "outs": []}
76 | obj["version"] = read_as_int(4)
77 | ins = read_var_int()
78 | for i in range(ins):
79 | obj["ins"].append({
80 | "outpoint": {
81 | "hash": read_bytes(32)[::-1],
82 | "index": read_as_int(4)
83 | },
84 | "script": read_var_string(),
85 | "sequence": read_as_int(4)
86 | })
87 | outs = read_var_int()
88 | for i in range(outs):
89 | obj["outs"].append({
90 | "value": read_as_int(8),
91 | "script": read_var_string()
92 | })
93 | obj["locktime"] = read_as_int(4)
94 | return obj
95 |
96 |
97 | def serialize(txobj):
98 | #if isinstance(txobj, bytes):
99 | # txobj = bytes_to_hex_string(txobj)
100 | o = []
101 | if json_is_base(txobj, 16):
102 | json_changedbase = json_changebase(txobj,
103 | lambda x: binascii.unhexlify(x))
104 | hexlified = safe_hexlify(serialize(json_changedbase))
105 | return hexlified
106 | o.append(encode(txobj["version"], 256, 4)[::-1])
107 | o.append(num_to_var_int(len(txobj["ins"])))
108 | for inp in txobj["ins"]:
109 | o.append(inp["outpoint"]["hash"][::-1])
110 | o.append(encode(inp["outpoint"]["index"], 256, 4)[::-1])
111 | o.append(num_to_var_int(len(inp["script"])) + (inp["script"] if inp[
112 | "script"] or is_python2 else bytes()))
113 | o.append(encode(inp["sequence"], 256, 4)[::-1])
114 | o.append(num_to_var_int(len(txobj["outs"])))
115 | for out in txobj["outs"]:
116 | o.append(encode(out["value"], 256, 8)[::-1])
117 | o.append(num_to_var_int(len(out["script"])) + out["script"])
118 | o.append(encode(txobj["locktime"], 256, 4)[::-1])
119 |
120 | return ''.join(o) if is_python2 else reduce(lambda x, y: x + y, o, bytes())
121 |
122 | # Hashing transactions for signing
123 |
124 | SIGHASH_ALL = 1
125 | SIGHASH_NONE = 2
126 | SIGHASH_SINGLE = 3
127 | # this works like SIGHASH_ANYONECANPAY | SIGHASH_ALL, might as well make it explicit while
128 | # we fix the constant
129 | SIGHASH_ANYONECANPAY = 0x81
130 |
131 |
132 | def signature_form(tx, i, script, hashcode=SIGHASH_ALL):
133 | i, hashcode = int(i), int(hashcode)
134 | if isinstance(tx, string_or_bytes_types):
135 | return serialize(signature_form(deserialize(tx), i, script, hashcode))
136 | newtx = copy.deepcopy(tx)
137 | for inp in newtx["ins"]:
138 | inp["script"] = ""
139 | newtx["ins"][i]["script"] = script
140 | if hashcode == SIGHASH_NONE:
141 | newtx["outs"] = []
142 | elif hashcode == SIGHASH_SINGLE:
143 | newtx["outs"] = newtx["outs"][:len(newtx["ins"])]
144 | for out in range(len(newtx["ins"]) - 1):
145 | out.value = 2**64 - 1
146 | out.script = ""
147 | elif hashcode == SIGHASH_ANYONECANPAY:
148 | newtx["ins"] = [newtx["ins"][i]]
149 | else:
150 | pass
151 | return newtx
152 |
153 | # Making the actual signatures
154 |
155 |
156 | def der_encode_sig(v, r, s):
157 | """Takes (vbyte, r, s) as ints and returns hex der encode sig"""
158 | #See https://github.com/vbuterin/pybitcointools/issues/89
159 | #See https://github.com/simcity4242/pybitcointools/
160 | s = N - s if s > N // 2 else s # BIP62 low s
161 | b1, b2 = encode(r, 256), encode(s, 256)
162 | if bytearray(b1)[
163 | 0] & 0x80: # add null bytes if leading byte interpreted as negative
164 | b1 = b'\x00' + b1
165 | if bytearray(b2)[0] & 0x80:
166 | b2 = b'\x00' + b2
167 | left = b'\x02' + encode(len(b1), 256, 1) + b1
168 | right = b'\x02' + encode(len(b2), 256, 1) + b2
169 | return safe_hexlify(b'\x30' + encode(
170 | len(left + right), 256, 1) + left + right)
171 |
172 |
173 | def der_decode_sig(sig):
174 | leftlen = decode(sig[6:8], 16) * 2
175 | left = sig[8:8 + leftlen]
176 | rightlen = decode(sig[10 + leftlen:12 + leftlen], 16) * 2
177 | right = sig[12 + leftlen:12 + leftlen + rightlen]
178 | return (None, decode(left, 16), decode(right, 16))
179 |
180 |
181 | def txhash(tx, hashcode=None):
182 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
183 | tx = changebase(tx, 16, 256)
184 | if hashcode:
185 | return dbl_sha256(from_string_to_bytes(tx) + encode(
186 | int(hashcode), 256, 4)[::-1])
187 | else:
188 | return safe_hexlify(bin_dbl_sha256(tx)[::-1])
189 |
190 |
191 | def bin_txhash(tx, hashcode=None):
192 | return binascii.unhexlify(txhash(tx, hashcode))
193 |
194 |
195 | def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL):
196 | rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv)
197 | return der_encode_sig(*rawsig) + encode(hashcode, 16, 2)
198 |
199 |
200 | def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL):
201 | return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub)
202 |
203 | # Scripts
204 |
205 | def mk_pubkey_script(addr):
206 | # Keep the auxiliary functions around for altcoins' sake
207 | return '76a914' + b58check_to_hex(addr) + '88ac'
208 |
209 |
210 | def mk_scripthash_script(addr):
211 | return 'a914' + b58check_to_hex(addr) + '87'
212 |
213 | # Address representation to output script
214 |
215 |
216 | def address_to_script(addr):
217 | if addr[0] == '3' or addr[0] == '2':
218 | return mk_scripthash_script(addr)
219 | else:
220 | return mk_pubkey_script(addr)
221 |
222 | # Output script to address representation
223 |
224 |
225 | def script_to_address(script, vbyte=0):
226 | if re.match('^[0-9a-fA-F]*#39;, script):
227 | script = binascii.unhexlify(script)
228 | if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(
229 | script) == 25:
230 | return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses
231 | else:
232 | if vbyte in [111, 196]:
233 | # Testnet
234 | scripthash_byte = 196
235 | else:
236 | scripthash_byte = 5
237 | # BIP0016 scripthash addresses
238 | return bin_to_b58check(script[2:-1], scripthash_byte)
239 |
240 |
241 | def p2sh_scriptaddr(script, magicbyte=5):
242 | if re.match('^[0-9a-fA-F]*#39;, script):
243 | script = binascii.unhexlify(script)
244 | return hex_to_b58check(hash160(script), magicbyte)
245 |
246 |
247 | scriptaddr = p2sh_scriptaddr
248 |
249 |
250 | def deserialize_script(script):
251 | if isinstance(script, str) and re.match('^[0-9a-fA-F]*#39;, script):
252 | return json_changebase(
253 | deserialize_script(binascii.unhexlify(script)),
254 | lambda x: safe_hexlify(x))
255 | out, pos = [], 0
256 | while pos < len(script):
257 | code = from_byte_to_int(script[pos])
258 | if code == 0:
259 | out.append(None)
260 | pos += 1
261 | elif code <= 75:
262 | out.append(script[pos + 1:pos + 1 + code])
263 | pos += 1 + code
264 | elif code <= 78:
265 | szsz = pow(2, code - 76)
266 | sz = decode(script[pos + szsz:pos:-1], 256)
267 | out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz])
268 | pos += 1 + szsz + sz
269 | elif code <= 96:
270 | out.append(code - 80)
271 | pos += 1
272 | else:
273 | out.append(code)
274 | pos += 1
275 | return out
276 |
277 |
278 | def serialize_script_unit(unit):
279 | if isinstance(unit, int):
280 | if unit < 16:
281 | return from_int_to_byte(unit + 80)
282 | else:
283 | return bytes([unit])
284 | elif unit is None:
285 | return b'\x00'
286 | else:
287 | if len(unit) <= 75:
288 | return from_int_to_byte(len(unit)) + unit
289 | elif len(unit) < 256:
290 | return from_int_to_byte(76) + from_int_to_byte(len(unit)) + unit
291 | elif len(unit) < 65536:
292 | return from_int_to_byte(77) + encode(len(unit), 256, 2)[::-1] + unit
293 | else:
294 | return from_int_to_byte(78) + encode(len(unit), 256, 4)[::-1] + unit
295 |
296 |
297 | if is_python2:
298 |
299 | def serialize_script(script):
300 | if json_is_base(script, 16):
301 | return binascii.hexlify(serialize_script(json_changebase(
302 | script, lambda x: binascii.unhexlify(x))))
303 | return ''.join(map(serialize_script_unit, script))
304 | else:
305 |
306 | def serialize_script(script):
307 | if json_is_base(script, 16):
308 | return safe_hexlify(serialize_script(json_changebase(
309 | script, lambda x: binascii.unhexlify(x))))
310 |
311 | result = bytes()
312 | for b in map(serialize_script_unit, script):
313 | result += b if isinstance(b, bytes) else bytes(b, 'utf-8')
314 | return result
315 |
316 |
317 | def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k
318 | if isinstance(args[0], list):
319 | pubs, k = args[0], int(args[1])
320 | else:
321 | pubs = list(filter(lambda x: len(str(x)) >= 32, args))
322 | k = int(args[len(pubs)])
323 | return serialize_script([k] + pubs + [len(pubs)]) + 'ae'
324 |
325 | # Signing and verifying
326 |
327 |
328 | def verify_tx_input(tx, i, script, sig, pub):
329 | if re.match('^[0-9a-fA-F]*#39;, tx):
330 | tx = binascii.unhexlify(tx)
331 | if re.match('^[0-9a-fA-F]*#39;, script):
332 | script = binascii.unhexlify(script)
333 | if not re.match('^[0-9a-fA-F]*#39;, sig):
334 | sig = safe_hexlify(sig)
335 | hashcode = decode(sig[-2:], 16)
336 | modtx = signature_form(tx, int(i), script, hashcode)
337 | return ecdsa_tx_verify(modtx, sig, pub, hashcode)
338 |
339 |
340 | def sign(tx, i, priv, hashcode=SIGHASH_ALL):
341 | i = int(i)
342 | if (not is_python2 and isinstance(re, bytes)) or not re.match(
343 | '^[0-9a-fA-F]*#39;, tx):
344 | return binascii.unhexlify(sign(safe_hexlify(tx), i, priv))
345 | if len(priv) <= 33:
346 | priv = safe_hexlify(priv)
347 | pub = privkey_to_pubkey(priv)
348 | address = pubkey_to_address(pub)
349 | signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode)
350 | sig = ecdsa_tx_sign(signing_tx, priv, hashcode)
351 | txobj = deserialize(tx)
352 | txobj["ins"][i]["script"] = serialize_script([sig, pub])
353 | return serialize(txobj)
354 |
355 |
356 | def signall(tx, priv):
357 | # if priv is a dictionary, assume format is
358 | # { 'txinhash:txinidx' : privkey }
359 | if isinstance(priv, dict):
360 | for e, i in enumerate(deserialize(tx)["ins"]):
361 | k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])]
362 | tx = sign(tx, e, k)
363 | else:
364 | for i in range(len(deserialize(tx)["ins"])):
365 | tx = sign(tx, i, priv)
366 | return tx
367 |
368 |
369 | def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL):
370 | if re.match('^[0-9a-fA-F]*#39;, tx):
371 | tx = binascii.unhexlify(tx)
372 | if re.match('^[0-9a-fA-F]*#39;, script):
373 | script = binascii.unhexlify(script)
374 | modtx = signature_form(tx, i, script, hashcode)
375 | return ecdsa_tx_sign(modtx, pk, hashcode)
376 |
377 |
378 | def apply_multisignatures(*args):
379 | # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n]
380 | tx, i, script = args[0], int(args[1]), args[2]
381 | sigs = args[3] if isinstance(args[3], list) else list(args[3:])
382 |
383 | if isinstance(script, str) and re.match('^[0-9a-fA-F]*#39;, script):
384 | script = binascii.unhexlify(script)
385 | sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs]
386 | if isinstance(tx, str) and re.match('^[0-9a-fA-F]*#39;, tx):
387 | return safe_hexlify(apply_multisignatures(
388 | binascii.unhexlify(tx), i, script, sigs))
389 |
390 | txobj = deserialize(tx)
391 | txobj["ins"][i]["script"] = serialize_script([None] + sigs + [script])
392 | return serialize(txobj)
393 |
394 |
395 | def is_inp(arg):
396 | return len(arg) > 64 or "output" in arg or "outpoint" in arg
397 |
398 |
399 | def mktx(*args):
400 | # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ...
401 | ins, outs = [], []
402 | for arg in args:
403 | if isinstance(arg, list):
404 | for a in arg:
405 | (ins if is_inp(a) else outs).append(a)
406 | else:
407 | (ins if is_inp(arg) else outs).append(arg)
408 |
409 | txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []}
410 | for i in ins:
411 | if isinstance(i, dict) and "outpoint" in i:
412 | txobj["ins"].append(i)
413 | else:
414 | if isinstance(i, dict) and "output" in i:
415 | i = i["output"]
416 | txobj["ins"].append({
417 | "outpoint": {"hash": i[:64],
418 | "index": int(i[65:])},
419 | "script": "",
420 | "sequence": 4294967295
421 | })
422 | for o in outs:
423 | if isinstance(o, string_or_bytes_types):
424 | addr = o[:o.find(':')]
425 | val = int(o[o.find(':') + 1:])
426 | o = {}
427 | if re.match('^[0-9a-fA-F]*#39;, addr):
428 | o["script"] = addr
429 | else:
430 | o["address"] = addr
431 | o["value"] = val
432 |
433 | outobj = {}
434 | if "address" in o:
435 | outobj["script"] = address_to_script(o["address"])
436 | elif "script" in o:
437 | outobj["script"] = o["script"]
438 | else:
439 | raise Exception("Could not find 'address' or 'script' in output.")
440 | outobj["value"] = o["value"]
441 | txobj["outs"].append(outobj)
442 |
443 | return serialize(txobj)
444 |
445 |
446 | def select(unspent, value):
447 | value = int(value)
448 | high = [u for u in unspent if u["value"] >= value]
449 | high.sort(key=lambda u: u["value"])
450 | low = [u for u in unspent if u["value"] < value]
451 | low.sort(key=lambda u: -u["value"])
452 | if len(high):
453 | return [high[0]]
454 | i, tv = 0, 0
455 | while tv < value and i < len(low):
456 | tv += low[i]["value"]
457 | i += 1
458 | if tv < value:
459 | raise Exception("Not enough funds")
460 | return low[:i]
461 |
462 | # Only takes inputs of the form { "output": blah, "value": foo }
463 |
464 |
465 | def mksend(*args):
466 | argz, change, fee = args[:-2], args[-2], int(args[-1])
467 | ins, outs = [], []
468 | for arg in argz:
469 | if isinstance(arg, list):
470 | for a in arg:
471 | (ins if is_inp(a) else outs).append(a)
472 | else:
473 | (ins if is_inp(arg) else outs).append(arg)
474 |
475 | isum = sum([i["value"] for i in ins])
476 | osum, outputs2 = 0, []
477 | for o in outs:
478 | if isinstance(o, string_types):
479 | o2 = {"address": o[:o.find(':')], "value": int(o[o.find(':') + 1:])}
480 | else:
481 | o2 = o
482 | outputs2.append(o2)
483 | osum += o2["value"]
484 |
485 | if isum < osum + fee:
486 | raise Exception("Not enough money")
487 | elif isum > osum + fee + 5430:
488 | outputs2 += [{"address": change, "value": isum - osum - fee}]
489 |
490 | return mktx(ins, outputs2)
491 |
--------------------------------------------------------------------------------
/electrumpersonalserver/certs/cert.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDBjCCAe4CCQDRr9lL8wL3+jANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
4 | cyBQdHkgTHRkMB4XDTE4MDMxMzAwMDc1OFoXDTIzMDMxMjAwMDc1OFowRTELMAkG
5 | A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
6 | IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
7 | ALvqB0SrsncAhIkvmkLhhE4GW0MKGQognd1Vm67gnOZ4rC2+HwF3xS1xpJDZIjRJ
8 | Tump6bp4dFoyty2vsrWJzzBib3xPh6yx37cqkQkLS7mlvl5J+VaV3vefW9Jt84Me
9 | hT2xH62haZ6gyAI2OItyoC57uiKGJ/4TNcXZRchc3Ee1R29q5evMjJRBG6QLfHmc
10 | btsSquPwIoGxaTICNZmmZlHKct+6bBGGbJ1tJttWCeLQe//rW9KBs+Ip3heNxZpQ
11 | RZgPYwxFhZg4g4nv0CmIeMCC8KBZE82P4SRjMirVZD0YH/nNYQTEGwutZZ8F0K8Z
12 | pWR1/HDXLQVeF66SreRxp98CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATELegjEF
13 | 03r1SWSsJ2Jhg4gLDLi3tORqHFzu3S8JECi57geZKmF7NmhJHXd0vemusX9P8IRh
14 | A0B9mEU8e92k2fenqxj/t71rm+e71PBU2GwXsE5vtkX/AGmsk6KhAsbe56A3IUGK
15 | JvS3HZ2JS9MmwoUI3cHB5ExBisk4YkDripA6cIkF4i9ctQbk3bzN7FhCbj9wrKBR
16 | Lg48tbswQw4UUUZ70U5uW3pt9pPbDErSifBv6UMoY8A2s4RqiyXDyI/cxVFsY71A
17 | c+tInksyG4KAeDo919/Y0bNjgzOCt267ApQE96j4lnGCx/KSwduWBNMng6qeTecx
18 | ZOyPoABtFZcv1g==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/electrumpersonalserver/certs/cert.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEogIBAAKCAQEAu+oHRKuydwCEiS+aQuGETgZbQwoZCiCd3VWbruCc5nisLb4f
3 | AXfFLXGkkNkiNElO6anpunh0WjK3La+ytYnPMGJvfE+HrLHftyqRCQtLuaW+Xkn5
4 | VpXe959b0m3zgx6FPbEfraFpnqDIAjY4i3KgLnu6IoYn/hM1xdlFyFzcR7VHb2rl
5 | 68yMlEEbpAt8eZxu2xKq4/AigbFpMgI1maZmUcpy37psEYZsnW0m21YJ4tB7/+tb
6 | 0oGz4ineF43FmlBFmA9jDEWFmDiDie/QKYh4wILwoFkTzY/hJGMyKtVkPRgf+c1h
7 | BMQbC61lnwXQrxmlZHX8cNctBV4XrpKt5HGn3wIDAQABAoIBAHjQSuH8nZ3i6FMn
8 | Fr+/LAfaEFy2pkiblcNSoeg6IsYOeWxjWp3f+hZwhQRXhaUmKKUUB+BKR0wiZSDr
9 | YDNVKa8K6nB61VjTd2jU5jBxYbs284C9gKAJdTOw8iEFbdU0DygNs7c3GqfQ6SZ6
10 | 47nL9W5NP+uoYxf4E89jFHlwMnOq3n2vW3TUIys0Op3RG40VqKEhU/XsOIjU5B7C
11 | HN2wMKMol3N62t8rbX/J7PaWxR3O017kim1Xz0fSbYA+nHk6SvA2rXMdShACcBRz
12 | t/DK4TzjwUL3a7P8Lvw8cfe0C6pBQD1sAcUxw4CQBcCs/PMCModJWoUmCflZZTqe
13 | +WdDA9kCgYEA8yvQyVhK4xMAWzCzaRjBNpFpFJAX6XAHjMmhnOCLNe2m7SQdi5p4
14 | /j65P9pjRv+nD07cv/7mrYAWXFXkLgz3WWObR4A5eWjA0gnyIHJGNa9NQ7vCWu6g
15 | aQpI+hnnwShakxuQ7w9w1WKAXh0rvnWO4hOxHlr5ve7UpEN18s7uzQMCgYEAxdPu
16 | b38Qb2tGUXWQOBYsJNvhdwevO8EPggP044ENJvQW0gmJuVsF/Yrua0TNrUv+ETYd
17 | mgK6MhNlxR6v3STwDg/OpM+MvgBafKRX2PeBJTf6k3Yx7ykxhLNPmuL6RCVv3ou9
18 | F064UzzyM9ApyWna7b0+1R1XPS+VttWZliLefPUCgYAb0iV/A7T9qczeogHEwmpI
19 | nfZRvfKeaIzUlLUCx8Xlk50HgJxIvpGdNPvozEmTc+hfHfyvkrA9pWvpgIIsqpsa
20 | BQVc9tSciVmWLkEfaTOTLM1ANJkV4jtECUM0KgaT2NQUBJFeaHvWTgC1w8yfa7+/
21 | KdWXzXzJOCvn5zf1Yat8lQKBgGgsfP+rqqzxkZrtzJ8sVdynCSiUHFvcA12U1c1D
22 | tPhRSv8Z1LON0i68jWZhWemq/cR0ecwTKZebDVlrGnLas6rD+i5huRyItR2zsSro
23 | 0tIVk1c5w3vMdm4Jup62bdGa4TkQ3uc6Jeh3TJeqQ4bzvjy5DjBNfhYTS8R24KTm
24 | AcFNAoGAPF4fOuTeFwy+a+LZ5ZTSHTiQ5YZYmnu27mM4YS0KjWCs2UKYKr47tOhf
25 | 96kuaN6zcE7gqawMTDGW+77DxTwKpbIEvKRb03PfqGnD8SHQmAQCjLOlnhW816n/
26 | +AtLlzWB7dBtgO/cW9lFIzyPCVlXRcbsn3RKS5HnLgIkuXn/kZI=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/__init__.py:
--------------------------------------------------------------------------------
1 | from electrumpersonalserver.server.merkleproof import (
2 | convert_core_to_electrum_merkle_proof
3 | )
4 | from electrumpersonalserver.server.jsonrpc import JsonRpc, JsonRpcError
5 | from electrumpersonalserver.server.hashes import (
6 | to_bytes,
7 | sha256,
8 | bh2u,
9 | script_to_scripthash,
10 | get_status_electrum,
11 | bfh,
12 | hash_encode,
13 | hash_decode,
14 | Hash,
15 | hash_merkle_root,
16 | hash_160,
17 | script_to_address,
18 | address_to_script,
19 | address_to_scripthash,
20 | bytes_fmt,
21 | )
22 | from electrumpersonalserver.server.transactionmonitor import (
23 | TransactionMonitor,
24 | )
25 | from electrumpersonalserver.server.deterministicwallet import (
26 | parse_electrum_master_public_key,
27 | DeterministicWallet,
28 | DescriptorDeterministicWallet,
29 | import_addresses,
30 | ADDRESSES_LABEL,
31 | )
32 | from electrumpersonalserver.server.socks import (
33 | socksocket,
34 | setdefaultproxy,
35 | PROXY_TYPE_SOCKS5,
36 | )
37 | from electrumpersonalserver.server.peertopeer import (
38 | tor_broadcast_tx,
39 | )
40 | from electrumpersonalserver.server.electrumprotocol import (
41 | SERVER_VERSION_NUMBER,
42 | UnknownScripthashError,
43 | ElectrumProtocol,
44 | get_block_header,
45 | get_current_header,
46 | get_block_headers_hex,
47 | DONATION_ADDR,
48 | )
49 | from electrumpersonalserver.server.mempoolhistogram import (
50 | MempoolSync
51 | )
52 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/deterministicwallet.py:
--------------------------------------------------------------------------------
1 |
2 | import logging
3 |
4 | import electrumpersonalserver.bitcoin as btc
5 | from electrumpersonalserver.server.hashes import bh2u, hash_160, bfh, sha256,\
6 | address_to_script, script_to_address
7 | from electrumpersonalserver.server.jsonrpc import JsonRpcError
8 |
9 | #the wallet types are here
10 | #https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES
11 | #and
12 | #https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
13 |
14 | ADDRESSES_LABEL = "electrum-watchonly-addresses"
15 |
16 | def import_addresses(rpc, watchonly_addrs, wallets, change_param, count,
17 | logger=None):
18 | """
19 | change_param = 0 for receive, 1 for change, -1 for both
20 | """
21 | logger = logger if logger else logging.getLogger('ELECTRUMPERSONALSERVER')
22 | logger.debug("Importing " + str(len(watchonly_addrs)) + " watch-only "
23 | + "address[es] and " + str(len(wallets)) + " wallet[s] into label \""
24 | + ADDRESSES_LABEL + "\"")
25 |
26 | watchonly_addr_param = [{"scriptPubKey": {"address": addr}, "label":
27 | ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"}
28 | for addr in watchonly_addrs]
29 | rpc.call("importmulti", [watchonly_addr_param, {"rescan": False}])
30 |
31 | for i, wal in enumerate(wallets):
32 | logger.info("Importing wallet " + str(i+1) + "/" + str(len(wallets)))
33 | if isinstance(wal, DescriptorDeterministicWallet):
34 | if change_param in (0, -1):
35 | #import receive addrs
36 | rpc.call("importmulti", [[{"desc": wal.descriptors[0], "range":
37 | [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True,
38 | "timestamp": "now"}], {"rescan": False}])
39 | if change_param in (1, -1):
40 | #import change addrs
41 | rpc.call("importmulti", [[{"desc": wal.descriptors[1], "range":
42 | [0, count-1], "label": ADDRESSES_LABEL, "watchonly": True,
43 | "timestamp": "now"}], {"rescan": False}])
44 | else:
45 | #old-style-seed wallets
46 | logger.info("importing an old-style-seed wallet, will be slow...")
47 | for change in [0, 1]:
48 | addrs, spks = wal.get_addresses(change, 0, count)
49 | addr_param = [{"scriptPubKey": {"address": a}, "label":
50 | ADDRESSES_LABEL, "watchonly": True, "timestamp": "now"}
51 | for a in addrs]
52 | rpc.call("importmulti", [addr_param, {"rescan": False}])
53 | logger.debug("Importing done")
54 |
55 |
56 | def is_string_parsable_as_hex_int(s):
57 | try:
58 | int(s, 16)
59 | return True
60 | except:
61 | return False
62 |
63 | def parse_electrum_master_public_key(keydata, gaplimit, rpc, chain):
64 | if chain == "main":
65 | xpub_vbytes = b"\x04\x88\xb2\x1e"
66 | elif chain == "test" or chain == "regtest" or chain == "signet":
67 | xpub_vbytes = b"\x04\x35\x87\xcf"
68 | else:
69 | assert False
70 |
71 | #https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
72 |
73 | descriptor_template = None
74 | if keydata[:4] in ("xpub", "tpub"):
75 | descriptor_template = "pkh({xpub}/{change}/*)"
76 | elif keydata[:4] in ("zpub", "vpub"):
77 | descriptor_template = "wpkh({xpub}/{change}/*)"
78 | elif keydata[:4] in ("ypub", "upub"):
79 | descriptor_template = "sh(wpkh({xpub}/{change}/*))"
80 |
81 | if descriptor_template != None:
82 | wallet = SingleSigWallet(rpc, xpub_vbytes, keydata, descriptor_template)
83 | elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128:
84 | wallet = SingleSigOldMnemonicWallet(rpc, keydata)
85 | elif keydata.find(" ") != -1: #multiple keys = multisig
86 | chunks = keydata.split(" ")
87 | try:
88 | m = int(chunks[0])
89 | except ValueError:
90 | raise ValueError("Unable to parse m in multisig key data: "
91 | + chunks[0])
92 | pubkeys = chunks[1:]
93 | if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]):
94 | raise ValueError("Inconsistent master public key types")
95 | if pubkeys[0][:4] in ("xpub", "tpub"):
96 | descriptor_script = "sh(sortedmulti("
97 | elif pubkeys[0][:4] in ("Zpub", "Vpub"):
98 | descriptor_script = "wsh(sortedmulti("
99 | elif pubkeys[0][:4] in ("Ypub", "Upub"):
100 | descriptor_script = "sh(wsh(sortedmulti("
101 | wallet = MultisigWallet(rpc, xpub_vbytes, m, pubkeys, descriptor_script)
102 | else:
103 | raise ValueError("Unrecognized electrum mpk format: " + keydata[:4])
104 | wallet.gaplimit = gaplimit
105 | return wallet
106 |
107 | class DeterministicWallet(object):
108 | def __init__(self, rpc):
109 | self.gaplimit = 0
110 | self.next_index = [0, 0]
111 | self.scriptpubkey_index = {}
112 | self.rpc = rpc
113 |
114 | def _derive_addresses(self, change, from_index, count):
115 | raise RuntimeError()
116 |
117 | def get_addresses(self, change, from_index, count):
118 | """Returns addresses from this deterministic wallet"""
119 | addrs = self._derive_addresses(change, from_index, count)
120 | spks = [address_to_script(a, self.rpc) for a in addrs]
121 | for index, spk in enumerate(spks):
122 | self.scriptpubkey_index[spk] = (change, from_index + index)
123 | self.next_index[change] = max(self.next_index[change], from_index+count)
124 | return addrs, spks
125 |
126 | def get_new_addresses(self, change, count):
127 | """Returns newly-generated addresses from this deterministic wallet"""
128 | addrs, spks = self.get_addresses(change, self.next_index[change], count)
129 | return addrs, spks
130 |
131 | #called in check_for_new_txes() when a new tx of ours arrives
132 | #to see if we need to import more addresses
133 | def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys):
134 | """Return None if they havent, or how many addresses to
135 | import if they have"""
136 | result = {}
137 | for spk in scriptpubkeys:
138 | if spk not in self.scriptpubkey_index:
139 | continue
140 | change, index = self.scriptpubkey_index[spk]
141 | distance_from_next = self.next_index[change] - index
142 | if distance_from_next > self.gaplimit:
143 | continue
144 | #need to import more
145 | if change in result:
146 | result[change] = max(result[change], self.gaplimit
147 | - distance_from_next + 1)
148 | else:
149 | result[change] = self.gaplimit - distance_from_next + 1
150 | if len(result) > 0:
151 | return result
152 | else:
153 | return None
154 |
155 | def rewind_one(self, change):
156 | """Go back one pubkey in a branch"""
157 | self.next_index[change] -= 1
158 |
159 | class DescriptorDeterministicWallet(DeterministicWallet):
160 | def __init__(self, rpc, xpub_vbytes, *args):
161 | super(DescriptorDeterministicWallet, self).__init__(rpc)
162 | self.xpub_vbytes = xpub_vbytes
163 |
164 | descriptors_without_checksum = \
165 | self.obtain_descriptors_without_checksum(args)
166 |
167 | try:
168 | self.descriptors = []
169 | for desc in descriptors_without_checksum:
170 | self.descriptors.append(self.rpc.call("getdescriptorinfo",
171 | [desc])["descriptor"])
172 | except JsonRpcError as e:
173 | raise ValueError(repr(e))
174 |
175 | def obtain_descriptors_without_checksum(self, *args):
176 | raise RuntimeError()
177 |
178 | def _derive_addresses(self, change, from_index, count):
179 | return self.rpc.call("deriveaddresses", [self.descriptors[change], [
180 | from_index, from_index + count - 1]])
181 | ##the minus 1 is because deriveaddresses uses inclusive range
182 | ##e.g. to get just the first address you use [0, 0]
183 |
184 | def _convert_to_standard_xpub(self, mpk):
185 | return btc.bip32_serialize((self.xpub_vbytes, *btc.bip32_deserialize(
186 | mpk)[1:]))
187 |
188 | class SingleSigWallet(DescriptorDeterministicWallet):
189 | def __init__(self, rpc, xpub_vbytes, xpub, descriptor_template):
190 | super(SingleSigWallet, self).__init__(rpc, xpub_vbytes, xpub,
191 | descriptor_template)
192 |
193 | def obtain_descriptors_without_checksum(self, args):
194 | ##example descriptor_template:
195 | #"pkh({xpub}/{change}/*)"
196 | xpub, descriptor_template = args
197 |
198 | descriptors_without_checksum = []
199 | xpub = self._convert_to_standard_xpub(xpub)
200 | for change in [0, 1]:
201 | descriptors_without_checksum.append(descriptor_template.format(
202 | change=change, xpub=xpub))
203 | return descriptors_without_checksum
204 |
205 | class MultisigWallet(DescriptorDeterministicWallet):
206 | def __init__(self, rpc, xpub_vbytes, m, xpub_list, descriptor_script):
207 | super(MultisigWallet, self).__init__(rpc, xpub_vbytes, m, xpub_list,
208 | descriptor_script)
209 |
210 | def obtain_descriptors_without_checksum(self, args):
211 | ##example descriptor_script:
212 | #"sh(sortedmulti("
213 | m, xpub_list, descriptor_script = args
214 |
215 | descriptors_without_checksum = []
216 | xpub_list = [self._convert_to_standard_xpub(xpub) for xpub in xpub_list]
217 | for change in [0, 1]:
218 | descriptors_without_checksum.append(descriptor_script + str(m) +\
219 | "," + ",".join([xpub + "/" + str(change) + "/*"
220 | for xpub in xpub_list]) + ")"*descriptor_script.count("("))
221 | return descriptors_without_checksum
222 |
223 | class SingleSigOldMnemonicWallet(DeterministicWallet):
224 | def __init__(self, rpc, mpk):
225 | super(SingleSigOldMnemonicWallet, self).__init__(rpc)
226 | self.mpk = mpk
227 |
228 | def _pubkey_to_scriptpubkey(self, pubkey):
229 | pkh = bh2u(hash_160(bfh(pubkey)))
230 | #op_dup op_hash_160 length hash160 op_equalverify op_checksig
231 | return "76a914" + pkh + "88ac"
232 |
233 | def _derive_addresses(self, change, from_index, count):
234 | result = []
235 | for index in range(from_index, from_index + count):
236 | pubkey = btc.electrum_pubkey(self.mpk, index, change)
237 | scriptpubkey = self._pubkey_to_scriptpubkey(pubkey)
238 | result.append(script_to_address(scriptpubkey, self.rpc))
239 | return result
240 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/hashes.py:
--------------------------------------------------------------------------------
1 |
2 | import hashlib
3 | import binascii
4 | import math
5 |
6 | ## stuff copied from electrum's source
7 |
8 | def to_bytes(something, encoding='utf8'):
9 | """
10 | cast string to bytes() like object, but for python2 support
11 | it's bytearray copy
12 | """
13 | if isinstance(something, bytes):
14 | return something
15 | if isinstance(something, str):
16 | return something.encode(encoding)
17 | elif isinstance(something, bytearray):
18 | return bytes(something)
19 | else:
20 | raise TypeError("Not a string or bytes like object")
21 |
22 | def sha256(x):
23 | x = to_bytes(x, 'utf8')
24 | return bytes(hashlib.sha256(x).digest())
25 |
26 | def bh2u(x):
27 | return binascii.hexlify(x).decode('ascii')
28 |
29 | def script_to_scripthash(script):
30 | """Electrum uses a format hash(scriptPubKey) as the index keys"""
31 | h = sha256(bytes.fromhex(script))[0:32]
32 | return bh2u(bytes(reversed(h)))
33 |
34 | #the 'result' field in the blockchain.scripthash.subscribe method
35 | # reply uses this as a summary of the address
36 | def get_status_electrum(h):
37 | if not h:
38 | return None
39 | if len(h) == 0:
40 | return None
41 | status = ''
42 | for tx_hash, height in h:
43 | status += tx_hash + ':%d:' % height
44 | return bh2u(hashlib.sha256(status.encode('ascii')).digest())
45 |
46 | bfh = bytes.fromhex
47 | hash_encode = lambda x: bh2u(x[::-1])
48 | hash_decode = lambda x: bfh(x)[::-1]
49 |
50 | def Hash(x):
51 | x = to_bytes(x, 'utf8')
52 | out = bytes(sha256(sha256(x)))
53 | return out
54 |
55 | def hash_merkle_root(merkle_s, target_hash, pos):
56 | h = hash_decode(target_hash)
57 | for i in range(len(merkle_s)):
58 | item = merkle_s[i]
59 | h = Hash(hash_decode(item) + h) if ((pos >> i) & 1) else Hash(
60 | h + hash_decode(item))
61 | return hash_encode(h)
62 |
63 | def hash_160(public_key):
64 | try:
65 | md = hashlib.new('ripemd160')
66 | md.update(sha256(public_key))
67 | return md.digest()
68 | except BaseException:
69 | from . import ripemd
70 | md = ripemd.new(sha256(public_key))
71 | return md.digest()
72 |
73 | ## end of electrum copypaste
74 |
75 | def script_to_address(scriptPubKey, rpc):
76 | return rpc.call("decodescript", [scriptPubKey])["address"]
77 |
78 | def address_to_script(addr, rpc):
79 | return rpc.call("validateaddress", [addr])["scriptPubKey"]
80 |
81 | def address_to_scripthash(addr, rpc):
82 | return script_to_scripthash(address_to_script(addr, rpc))
83 |
84 | # doesnt really fit here but i dont want to clutter up server.py
85 |
86 | unit_list = list(zip(['B', 'kB', 'MB', 'GB', 'TB', 'PB'], [0, 0, 1, 2, 2, 2]))
87 |
88 | def bytes_fmt(num):
89 | """Human friendly file size"""
90 | if num > 1:
91 | exponent = min(int(math.log(num, 1000)), len(unit_list) - 1)
92 | quotient = float(num) / 1000**exponent
93 | unit, num_decimals = unit_list[exponent]
94 | format_string = '{:.%sf} {}' % (num_decimals)
95 | return format_string.format(quotient, unit)
96 | if num == 0:
97 | return '0 bytes'
98 | if num == 1:
99 | return '1 byte'
100 |
101 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/jsonrpc.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu>
2 | # Copyright (C) 2014 by phelix / blockchained.com
3 |
4 | #jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/jmclient/jmclient/jsonrpc.py
5 |
6 | import base64
7 | import http.client
8 | import json
9 |
10 | class JsonRpcError(Exception): pass
11 | class JsonRpcConnectionError(JsonRpcError): pass
12 |
13 | class JsonRpc(object):
14 | """
15 | Simple implementation of a JSON-RPC client that is used
16 | to connect to Bitcoin.
17 | """
18 | def __init__(self, host, port, user, password, cookie_path=None,
19 | wallet_filename="", logger=None):
20 | self.host = host
21 | self.port = port
22 |
23 | self.cookie_path = cookie_path
24 | if cookie_path:
25 | self.load_from_cookie()
26 | else:
27 | self.create_authstr(user, password)
28 |
29 | self.conn = http.client.HTTPConnection(self.host, self.port)
30 | if len(wallet_filename) > 0:
31 | self.url = "/wallet/" + wallet_filename
32 | else:
33 | self.url = ""
34 | self.logger = logger
35 | self.queryId = 1
36 |
37 | def create_authstr(self, username, password):
38 | self.authstr = "%s:%s" % (username, password)
39 |
40 | def load_from_cookie(self):
41 | fd = open(self.cookie_path)
42 | username, password = fd.read().strip().split(":")
43 | fd.close()
44 | self.create_authstr(username, password)
45 |
46 | def queryHTTP(self, obj):
47 | """
48 | Send an appropriate HTTP query to the server. The JSON-RPC
49 | request should be (as object) in 'obj'. If the call succeeds,
50 | the resulting JSON object is returned. In case of an error
51 | with the connection (not JSON-RPC itself), an exception is raised.
52 | """
53 | headers = {"User-Agent": "electrum-personal-server",
54 | "Content-Type": "application/json",
55 | "Accept": "application/json"}
56 | headers["Authorization"] = (b"Basic " +
57 | base64.b64encode(self.authstr.encode('utf-8')))
58 | body = json.dumps(obj)
59 | auth_failed_once = False
60 | for i in range(20):
61 | try:
62 | self.conn.request("POST", self.url, body, headers)
63 | response = self.conn.getresponse()
64 | if response.status == 401:
65 | if self.cookie_path == None or auth_failed_once:
66 | self.conn.close()
67 | raise JsonRpcConnectionError(
68 | "authentication for JSON-RPC failed")
69 | else:
70 | auth_failed_once = True
71 | #try reloading u/p from the cookie file once
72 | self.load_from_cookie()
73 | raise OSError() #jump to error handler below
74 | auth_failed_once = False
75 | #All the codes below are 'fine' from a JSON-RPC point of view.
76 | if response.status not in [200, 404, 500]:
77 | self.conn.close()
78 | raise JsonRpcConnectionError("unknown error in JSON-RPC")
79 | data = response.read()
80 | return json.loads(data.decode('utf-8'))
81 | except JsonRpcConnectionError as exc:
82 | raise exc
83 | except http.client.BadStatusLine:
84 | return "CONNFAILURE"
85 | except OSError:
86 | # connection dropped, reconnect
87 | try:
88 | self.conn.close()
89 | self.conn.connect()
90 | except ConnectionError as e:
91 | #node probably offline, notify with jsonrpc error
92 | raise JsonRpcConnectionError(repr(e))
93 | continue
94 | except Exception as exc:
95 | raise JsonRpcConnectionError("JSON-RPC connection failed. Err:"
96 | + repr(exc))
97 | break
98 | return None
99 |
100 | def call(self, method, params):
101 | currentId = self.queryId
102 | self.queryId += 1
103 |
104 | request = {"method": method, "params": params, "id": currentId}
105 | #query can fail from keepalive timeout; keep retrying if it does, up
106 | #to a reasonable limit, then raise (failure to access blockchain
107 | #is a critical failure). Note that a real failure to connect (e.g.
108 | #wrong port) is raised in queryHTTP directly.
109 | response_received = False
110 | for i in range(100):
111 | response = self.queryHTTP(request)
112 | if response != "CONNFAILURE":
113 | response_received = True
114 | break
115 | #Failure means keepalive timed out, just make a new one
116 | self.conn = http.client.HTTPConnection(self.host, self.port)
117 | if not response_received:
118 | raise JsonRpcConnectionError("Unable to connect over RPC")
119 | if response["id"] != currentId:
120 | raise JsonRpcConnectionError("invalid id returned by query")
121 | if response["error"] is not None:
122 | raise JsonRpcError(response["error"])
123 | return response["result"]
124 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/mempoolhistogram.py:
--------------------------------------------------------------------------------
1 |
2 | import time
3 | from collections import defaultdict
4 | from datetime import datetime
5 | from enum import Enum
6 |
7 | from electrumpersonalserver.server.jsonrpc import JsonRpcError
8 |
9 | def calc_histogram(mempool):
10 | #algorithm copied from the relevant place in ElectrumX
11 | #https://github.com/kyuupichan/electrumx/blob/e92c9bd4861c1e35989ad2773d33e01219d33280/server/mempool.py
12 | fee_hist = defaultdict(int)
13 | for fee_rate, size in mempool.values():
14 | fee_hist[fee_rate] += size
15 | l = list(reversed(sorted(fee_hist.items())))
16 | out = []
17 | size = 0
18 | r = 0
19 | binsize = 100000
20 | for fee, s in l:
21 | size += s
22 | if size + r > binsize:
23 | out.append((fee, size))
24 | r += size - binsize
25 | size = 0
26 | binsize *= 1.1
27 | return out
28 |
29 | class PollIntervalChange(Enum):
30 | UNCHANGED = "unchanged"
31 | FAST_POLLING = "fastpolling"
32 | NORMAL_POLLING = "normalpolling"
33 |
34 | class MempoolSync(object):
35 | def __init__(self, rpc, disabled, polling_interval):
36 | self.rpc = rpc
37 | self.disabled = disabled
38 | self.polling_interval = polling_interval
39 | self.mempool = dict()
40 | self.cached_fee_histogram = [[0, 0]]
41 | self.added_txids = None
42 | self.last_poll = None
43 | self.state = "gettxids"
44 |
45 | def set_polling_interval(self, polling_interval):
46 | self.polling_interval = polling_interval
47 |
48 | def get_fee_histogram(self):
49 | return self.cached_fee_histogram
50 |
51 | def initial_sync(self, logger):
52 | if self.disabled:
53 | return
54 | logger.info("Synchronizing mempool . . .")
55 | st = time.time()
56 | for _ in range(2):
57 | self.poll_update(-1)
58 | self.state = "gettxids"
59 | for _ in range(2):
60 | self.poll_update(-1)
61 | #run once for the getrawmempool
62 | #again for the getmempoolentry
63 | #and all that again because the first time will take so long
64 | # that new txes could arrive in that time
65 | et = time.time()
66 | logger.info("Found " + str(len(self.mempool)) + " mempool entries. "
67 | + "Synchronized mempool in " + str(et - st) + "sec")
68 |
69 | #-1 for no timeout
70 | def poll_update(self, timeout):
71 | poll_interval_change = PollIntervalChange.UNCHANGED
72 | if self.disabled:
73 | return poll_interval_change
74 | if self.state == "waiting":
75 | if ((datetime.now() - self.last_poll).total_seconds()
76 | > self.polling_interval):
77 | poll_interval_change = PollIntervalChange.FAST_POLLING
78 | self.state = "gettxids"
79 | elif self.state == "gettxids":
80 | mempool_txids = self.rpc.call("getrawmempool", [])
81 | self.last_poll = datetime.now()
82 | mempool_txids = set(mempool_txids)
83 |
84 | removed_txids = set(self.mempool.keys()).difference(mempool_txids)
85 | self.added_txids = iter(mempool_txids.difference(
86 | set(self.mempool.keys())))
87 |
88 | for txid in removed_txids:
89 | del self.mempool[txid]
90 |
91 | self.state = "getfeerates"
92 | elif self.state == "getfeerates":
93 | if timeout == -1:
94 | timeout = 2**32
95 | start_time = datetime.now()
96 | while self.state != "waiting" and ((datetime.now() - start_time
97 | ).total_seconds() < timeout):
98 | try:
99 | txid = next(self.added_txids)
100 | except StopIteration:
101 | self.cached_fee_histogram = calc_histogram(self.mempool)
102 | self.state = "waiting"
103 | poll_interval_change = \
104 | PollIntervalChange.NORMAL_POLLING
105 | self.last_poll = datetime.now()
106 | continue
107 | try:
108 | mempool_tx = self.rpc.call("getmempoolentry", [txid])
109 | except JsonRpcError:
110 | continue
111 | fee_rate = 1e8*mempool_tx["fees"]["base"] // mempool_tx["vsize"]
112 | self.mempool[txid] = (fee_rate, mempool_tx["vsize"])
113 |
114 | return poll_interval_change
115 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/merkleproof.py:
--------------------------------------------------------------------------------
1 |
2 | import electrumpersonalserver.bitcoin as btc
3 | import binascii
4 | from math import ceil, log
5 |
6 | from electrumpersonalserver.server.hashes import Hash, hash_encode, hash_decode
7 |
8 | #lots of ideas and code taken from bitcoin core and breadwallet
9 | #https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h
10 | #https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c
11 |
12 | def calc_tree_width(height, txcount):
13 | """Efficently calculates the number of nodes at given merkle tree height"""
14 | return (txcount + (1 << height) - 1) >> height
15 |
16 | def decend_merkle_tree(hashes, flags, height, txcount, pos):
17 | """Function recursively follows the flags bitstring down into the
18 | tree, building up a tree in memory"""
19 | flag = next(flags)
20 | if height > 0:
21 | #non-txid node
22 | if flag:
23 | left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2)
24 | #bitcoin's merkle tree format has a rule that if theres an
25 | # odd number of nodes in then the tree, the last hash is duplicated
26 | #in the electrum format we must hash together the duplicate
27 | # tree branch
28 | if pos*2+1 < calc_tree_width(height-1, txcount):
29 | right = decend_merkle_tree(hashes, flags, height-1,
30 | txcount, pos*2+1)
31 | else:
32 | if isinstance(left, tuple):
33 | right = expand_tree_hashing(left)
34 | else:
35 | right = left
36 | return (left, right)
37 | else:
38 | hs = next(hashes)
39 | return hs
40 | else:
41 | #txid node
42 | hs = next(hashes)
43 | if flag:
44 | #for the actual transaction, also store its position with a flag
45 | return "tx:" + str(pos) + ":" + hs
46 | else:
47 | return hs
48 |
49 | def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount):
50 | """Converts core's format for a merkle proof into a tree in memory"""
51 | tree_depth = int(ceil(log(txcount, 2)))
52 | hashes = iter(hash_list)
53 | #one-liner which converts the flags value to a list of True/False bits
54 | flags = (flag_value[i//8]&1 << i%8 != 0 for i in range(len(flag_value)*8))
55 | try:
56 | root_node = decend_merkle_tree(hashes, flags, tree_depth, txcount, 0)
57 | return root_node
58 | except StopIteration:
59 | raise ValueError
60 |
61 | def expand_tree_electrum_format_merkle_proof(node, result):
62 | """Recurse down into the tree, adding hashes to the result list
63 | in depth order"""
64 | left, right = node
65 | if isinstance(left, tuple):
66 | expand_tree_electrum_format_merkle_proof(left, result)
67 | if isinstance(right, tuple):
68 | expand_tree_electrum_format_merkle_proof(right, result)
69 | if not isinstance(left, tuple):
70 | result.append(left)
71 | if not isinstance(right, tuple):
72 | result.append(right)
73 |
74 | def get_node_hash(node):
75 | if node.startswith("tx"):
76 | return node.split(":")[2]
77 | else:
78 | return node
79 |
80 | def expand_tree_hashing(node):
81 | """Recurse down into the tree, hashing everything and
82 | returning root hash"""
83 | left, right = node
84 | if isinstance(left, tuple):
85 | hash_left = expand_tree_hashing(left)
86 | else:
87 | hash_left = get_node_hash(left)
88 | if isinstance(right, tuple):
89 | hash_right = expand_tree_hashing(right)
90 | else:
91 | hash_right = get_node_hash(right)
92 | return hash_encode(Hash(hash_decode(hash_left) + hash_decode(hash_right)))
93 |
94 | def convert_core_to_electrum_merkle_proof(proof):
95 | """Bitcoin Core and Electrum use different formats for merkle
96 | proof, this function converts from Core's format to Electrum's format"""
97 | proof = binascii.unhexlify(proof)
98 | pos = [0]
99 | def read_as_int(bytez):
100 | pos[0] += bytez
101 | return btc.decode(proof[pos[0] - bytez:pos[0]][::-1], 256)
102 | def read_var_int():
103 | pos[0] += 1
104 | val = btc.from_byte_to_int(proof[pos[0] - 1])
105 | if val < 253:
106 | return val
107 | return read_as_int(pow(2, val - 252))
108 | def read_bytes(bytez):
109 | pos[0] += bytez
110 | return proof[pos[0] - bytez:pos[0]]
111 |
112 | merkle_root = proof[36:36+32]
113 | pos[0] = 80
114 | txcount = read_as_int(4)
115 | hash_count = read_var_int()
116 | hashes = [hash_encode(read_bytes(32)) for i in range(hash_count)]
117 | flags_count = read_var_int()
118 | flags = read_bytes(flags_count)
119 |
120 | root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount)
121 | #check special case of a tree of zero height, block with only coinbase tx
122 | if not isinstance(root_node, tuple):
123 | root_node = root_node[5:] #remove the "tx:0:"
124 | result = {"pos": 0, "merkle": [], "txid": root_node,
125 | "merkleroot": hash_encode(merkle_root)}
126 | return result
127 |
128 | hashes_list = []
129 | expand_tree_electrum_format_merkle_proof(root_node, hashes_list)
130 | #remove the first or second element which is the txhash
131 | tx = hashes_list[0]
132 | if hashes_list[1].startswith("tx"):
133 | tx = hashes_list[1]
134 | assert(tx.startswith("tx"))
135 | hashes_list.remove(tx)
136 | #if the txhash was duplicated, that _is_ included in electrum's format
137 | if hashes_list[0].startswith("tx"):
138 | hashes_list[0] = tx.split(":")[2]
139 | tx_pos, txid = tx.split(":")[1:3]
140 | tx_pos = int(tx_pos)
141 | result = {"pos": tx_pos, "merkle": hashes_list, "txid": txid,
142 | "merkleroot": hash_encode(merkle_root)}
143 | return result
144 |
145 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/peertopeer.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | import socket
4 | import time
5 | import base64
6 | import threading
7 | import queue
8 | import random
9 | from struct import pack, unpack
10 | from datetime import datetime
11 |
12 | import electrumpersonalserver.bitcoin as btc
13 | from electrumpersonalserver.server.socks import (
14 | socksocket,
15 | setdefaultproxy,
16 | PROXY_TYPE_SOCKS5
17 | )
18 | from electrumpersonalserver.server.jsonrpc import JsonRpcError
19 |
20 | PROTOCOL_VERSION = 70016
21 | DEFAULT_USER_AGENT = '/Satoshi:22.0.0/'
22 |
23 | #https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h
24 | NODE_NETWORK = 1
25 | NODE_BLOOM = 1 << 2
26 | NODE_WITNESS = 1 << 3
27 | NODE_NETWORK_LIMITED = 1 << 10
28 |
29 | # protocol versions above this also send a relay boolean
30 | RELAY_TX_VERSION = 70001
31 |
32 | # length of bitcoin p2p packets
33 | HEADER_LENGTH = 24
34 |
35 | # if no message has been seen for this many seconds, send a ping
36 | KEEPALIVE_INTERVAL = 2 * 60
37 |
38 | # close connection if keep alive ping isnt responded to in this many seconds
39 | KEEPALIVE_TIMEOUT = 20 * 60
40 |
41 |
42 | def ip_to_hex(ip_str):
43 | # ipv4 only for now
44 | return socket.inet_pton(socket.AF_INET, ip_str)
45 |
46 | def create_net_addr(hexip, port): # doesnt contain time as in bitcoin wiki
47 | services = 0
48 | hex = bytes(10) + b'\xFF\xFF' + hexip
49 | return pack('<Q16s', services, hex) + pack('>H', port)
50 |
51 | def create_var_str(s):
52 | return btc.num_to_var_int(len(s)) + s.encode()
53 |
54 | def read_int(ptr, payload, n, littleendian=True):
55 | data = payload[ptr[0] : ptr[0]+n]
56 | if littleendian:
57 | data = data[::-1]
58 | ret = btc.decode(data, 256)
59 | ptr[0] += n
60 | return ret
61 |
62 | def read_var_int(ptr, payload):
63 | val = payload[ptr[0]]
64 | ptr[0] += 1
65 | if val < 253:
66 | return val
67 | return read_int(ptr, payload, 2**(val - 252))
68 |
69 | def read_var_str(ptr, payload):
70 | l = read_var_int(ptr, payload)
71 | ret = payload[ptr[0]: ptr[0] + l]
72 | ptr[0] += l
73 | return ret
74 |
75 | def ip_hex_to_str(ip_hex):
76 | # https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
77 | # https://www.cypherpunk.at/onioncat_trac/wiki/OnionCat
78 | if ip_hex[:14] == '\x00'*10 + '\xff'*2:
79 | # ipv4 mapped ipv6 addr
80 | return socket.inet_ntoa(ip_hex[12:])
81 | elif ip_hex[:6] == '\xfd\x87\xd8\x7e\xeb\x43':
82 | return base64.b32encode(ip_hex[6:]).lower() + '.onion'
83 | else:
84 | return socket.inet_ntop(socket.AF_INET6, ip_hex)
85 |
86 | class P2PMessageHandler(object):
87 | def __init__(self, logger):
88 | self.last_message = datetime.now()
89 | self.waiting_for_keepalive = False
90 | self.logger = logger
91 |
92 | def check_keepalive(self, p2p):
93 | if self.waiting_for_keepalive:
94 | if ((datetime.now() - self.last_message).total_seconds()
95 | < KEEPALIVE_TIMEOUT):
96 | return
97 | self.logger.debug('keepalive timed out, closing')
98 | p2p.sock.close()
99 | else:
100 | if ((datetime.now() - self.last_message).total_seconds()
101 | < KEEPALIVE_INTERVAL):
102 | return
103 | self.logger.debug('sending keepalive to peer')
104 | self.waiting_for_keepalive = True
105 | p2p.sock.sendall(p2p.create_message('ping', '\x00'*8))
106 |
107 | def handle_message(self, p2p, command, length, payload):
108 | self.last_message = datetime.now()
109 | self.waiting_for_keepalive = False
110 | ptr = [0]
111 | if command == b'version':
112 | version = read_int(ptr, payload, 4)
113 | services = read_int(ptr, payload, 8)
114 | timestamp = read_int(ptr, payload, 8)
115 | addr_recv_services = read_int(ptr, payload, 8)
116 | addr_recv_ip = payload[ptr[0] : ptr[0]+16]
117 | ptr[0] += 16
118 | addr_recv_port = read_int(ptr, payload, 2, False)
119 | addr_trans_services = read_int(ptr, payload, 8)
120 | addr_trans_ip = payload[ptr[0] : ptr[0]+16]
121 | ptr[0] += 16
122 | addr_trans_port = read_int(ptr, payload, 2, False)
123 | ptr[0] += 8 # skip over nonce
124 | user_agent = read_var_str(ptr, payload)
125 | start_height = read_int(ptr, payload, 4)
126 | if version > RELAY_TX_VERSION:
127 | relay = read_int(ptr, payload, 1) != 0
128 | else:
129 | # must check node accepts unconfirmed txes before broadcasting
130 | relay = True
131 | self.logger.debug(('Received peer version message: version=%d'
132 | + ' services=0x%x'
133 | + ' timestamp=%s user_agent=%s start_height=%d relay=%i'
134 | + ' them=%s:%d us=%s:%d') % (version,
135 | services, str(datetime.fromtimestamp(timestamp)),
136 | user_agent, start_height, relay, ip_hex_to_str(addr_trans_ip)
137 | , addr_trans_port, ip_hex_to_str(addr_recv_ip), addr_recv_port))
138 | p2p.sock.sendall(p2p.create_message('verack', b''))
139 | self.on_recv_version(p2p, version, services, timestamp,
140 | addr_recv_services, addr_recv_ip, addr_trans_services,
141 | addr_trans_ip, addr_trans_port, user_agent, start_height,
142 | relay)
143 | elif command == b'verack':
144 | self.on_connected(p2p)
145 | elif command == b'ping':
146 | p2p.sock.sendall(p2p.create_message('pong', payload))
147 |
148 | # optional override these in a subclass
149 |
150 | def on_recv_version(self, p2p, version, services, timestamp,
151 | addr_recv_services, addr_recv_ip, addr_trans_services,
152 | addr_trans_ip, addr_trans_port, user_agent, start_height, relay):
153 | pass
154 |
155 | def on_connected(self, p2p):
156 | pass
157 |
158 | def on_heartbeat(self, p2p):
159 | pass
160 |
161 |
162 | class P2PProtocol(object):
163 | def __init__(self, p2p_message_handler, remote_hostport,
164 | network, logger, notify_queue, user_agent=DEFAULT_USER_AGENT,
165 | socks5_hostport=("localhost", 9050), connect_timeout=30,
166 | heartbeat_interval=15, start_height=0):
167 | self.p2p_message_handler = p2p_message_handler
168 | self.remote_hostport = remote_hostport
169 | self.logger = logger
170 | self.notify_queue = notify_queue
171 | self.user_agent = user_agent
172 | self.socks5_hostport = socks5_hostport
173 | self.connect_timeout = connect_timeout
174 | self.heartbeat_interval = heartbeat_interval
175 | self.start_height = start_height
176 | if network == "testnet":
177 | self.magic = 0x0709110b
178 | elif network == "regtest":
179 | self.magic = 0xdab5bffa
180 | else:
181 | self.magic = 0xd9b4bef9
182 | self.closed = False
183 |
184 | def run(self):
185 | services = (NODE_NETWORK | NODE_WITNESS | NODE_NETWORK_LIMITED)
186 | st = int(time.time())
187 | nonce = random.getrandbits(64)
188 |
189 | netaddr_them = create_net_addr(ip_to_hex('0.0.0.0'), 0)
190 | netaddr_us = create_net_addr(ip_to_hex('0.0.0.0'), 0)
191 | version_message = (pack('<iQQ', PROTOCOL_VERSION, services, st)
192 | + netaddr_them
193 | + netaddr_us
194 | + pack('<Q', nonce)
195 | + create_var_str(self.user_agent)
196 | + pack('<I', self.start_height)
197 | + b'\x01')
198 |
199 | self.logger.debug('Connecting to bitcoin peer at ' +
200 | str(self.remote_hostport) + ' with proxy ' +
201 | str(self.socks5_hostport))
202 | setdefaultproxy(PROXY_TYPE_SOCKS5, self.socks5_hostport[0],
203 | self.socks5_hostport[1], True)
204 | self.sock = socksocket()
205 | self.sock.settimeout(self.connect_timeout)
206 | self.sock.connect(self.remote_hostport)
207 | self.sock.sendall(self.create_message('version', version_message))
208 |
209 | self.sock.settimeout(self.heartbeat_interval)
210 | self.closed = False
211 | try:
212 | recv_buffer = b''
213 | payload_length = -1 # -1 means waiting for header
214 | command = None
215 | checksum = None
216 | while not self.closed:
217 | try:
218 | recv_data = self.sock.recv(4096)
219 | if not recv_data or len(recv_data) == 0:
220 | raise EOFError()
221 | recv_buffer += recv_data
222 | # this is O(N^2) scaling in time, another way would be to
223 | # store in a list and combine at the end with "".join()
224 | # but this isnt really timing critical so didnt optimize it
225 |
226 | data_remaining = True
227 | while data_remaining and not self.closed:
228 | if payload_length == -1 and (len(recv_buffer)
229 | >= HEADER_LENGTH):
230 | net_magic, command, payload_length, checksum =\
231 | unpack('<I12sI4s', recv_buffer[:HEADER_LENGTH])
232 | recv_buffer = recv_buffer[HEADER_LENGTH:]
233 | if net_magic != self.magic:
234 | self.logger.debug('wrong MAGIC: ' +
235 | hex(net_magic))
236 | self.sock.close()
237 | break
238 | command = command.strip(b'\0')
239 | else:
240 | if payload_length >= 0 and (len(recv_buffer)
241 | >= payload_length):
242 | payload = recv_buffer[:payload_length]
243 | recv_buffer = recv_buffer[payload_length:]
244 | if btc.bin_dbl_sha256(payload)[:4] == checksum:
245 | self.p2p_message_handler.handle_message(
246 | self, command, payload_length, payload)
247 | else:
248 | self.logger.debug("wrong checksum, " +
249 | "dropping " +
250 | "message, cmd=" + command +
251 | " payloadlen=" + str(payload_length))
252 | payload_length = -1
253 | data_remaining = True
254 | else:
255 | data_remaining = False
256 | except socket.timeout:
257 | self.p2p_message_handler.check_keepalive(self)
258 | self.p2p_message_handler.on_heartbeat(self)
259 | except EOFError as e:
260 | self.closed = True
261 | except IOError as e:
262 | import traceback
263 | self.logger.debug("logging traceback from %s: \n" %
264 | traceback.format_exc())
265 | self.closed = True
266 | finally:
267 | try:
268 | self.sock.close()
269 | except Exception as _:
270 | pass
271 |
272 | def close(self):
273 | self.closed = True
274 |
275 | def create_message(self, command, payload):
276 | return (pack("<I12sI", self.magic, command.encode(), len(payload))
277 | + btc.bin_dbl_sha256(payload)[:4] + payload)
278 |
279 | class P2PBroadcastTx(P2PMessageHandler):
280 | def __init__(self, txhex, logger, notify_queue):
281 | P2PMessageHandler.__init__(self, logger)
282 | self.txhex = bytes.fromhex(txhex)
283 | self.txid = btc.bin_txhash(self.txhex)
284 | self.uploaded_tx = False
285 | self.time_marker = datetime.now()
286 | self.connected = False
287 | self.notify_queue = notify_queue
288 |
289 | def on_recv_version(self, p2p, version, services, timestamp,
290 | addr_recv_services, addr_recv_ip, addr_trans_services,
291 | addr_trans_ip, addr_trans_port, user_agent, start_height, relay):
292 | if not relay:
293 | self.logger.debug('peer not accepting unconfirmed txes, trying ' +
294 | 'another')
295 | # this happens if the other node is using blockonly=1
296 | p2p.close()
297 | if not services & NODE_WITNESS:
298 | self.logger.debug('peer not accepting witness data, trying another')
299 | p2p.close()
300 |
301 | def on_connected(self, p2p):
302 | MSG = 1 #msg_tx
303 | inv_payload = pack('<BI', 1, MSG) + self.txid
304 | p2p.sock.sendall(p2p.create_message('inv', inv_payload))
305 | self.time_marker = datetime.now()
306 | self.uploaded_tx = False
307 | self.connected = True
308 |
309 | def on_heartbeat(self, p2p):
310 | self.logger.debug('broadcaster heartbeat')
311 | VERACK_TIMEOUT = 40
312 | GETDATA_TIMEOUT = 60
313 | if not self.connected:
314 | if ((datetime.now() - self.time_marker).total_seconds()
315 | < VERACK_TIMEOUT):
316 | return
317 | self.logger.debug('timed out of waiting for verack')
318 | else:
319 | if ((datetime.now() - self.time_marker).total_seconds()
320 | < GETDATA_TIMEOUT):
321 | return
322 | self.logger.debug('timed out in waiting for getdata, node ' +
323 | 'already has tx')
324 | self.uploaded_tx = True
325 | p2p.close()
326 |
327 | def handle_message(self, p2p, command, length, payload):
328 | P2PMessageHandler.handle_message(self, p2p, command, length, payload)
329 | ptr = [0]
330 | if command == b'getdata':
331 | count = read_var_int(ptr, payload)
332 | for _ in range(count):
333 | ptr[0] += 4
334 | hash_id = payload[ptr[0] : ptr[0] + 32]
335 | ptr[0] += 32
336 | if hash_id == self.txid:
337 | p2p.sock.sendall(p2p.create_message('tx', self.txhex))
338 | self.uploaded_tx = True
339 | self.logger.debug("Uploaded transaction via tor to peer at "
340 | + str(p2p.remote_hostport))
341 | self.notify_queue.put(True)
342 | ##make sure the packets really got through by sleeping
343 | ##some kernels seem to send a RST packet on close() even
344 | ##if theres still data in the send buffer
345 | time.sleep(5)
346 | p2p.close()
347 |
348 | def broadcaster_thread(txhex, node_addrs, tor_hostport, network, logger,
349 | start_height, notify_queue):
350 | for node_addr in node_addrs:
351 | remote_hostport = (node_addr["address"], node_addr["port"])
352 | p2p_msg_handler = P2PBroadcastTx(txhex, logger, notify_queue)
353 | p2p = P2PProtocol(p2p_msg_handler, remote_hostport,
354 | network, logger, notify_queue, socks5_hostport=tor_hostport,
355 | heartbeat_interval=20, start_height=start_height)
356 | try:
357 | p2p.run()
358 | except IOError as e:
359 | logger.debug("p2p.run() exited: " + repr(e))
360 | continue
361 | if p2p_msg_handler.uploaded_tx:
362 | break
363 | logger.debug("Exiting tor broadcast thread, uploaded_tx = " +
364 | str(p2p_msg_handler.uploaded_tx))
365 | if not p2p_msg_handler.uploaded_tx:
366 | notify_queue.put(False)
367 | return p2p_msg_handler.uploaded_tx
368 |
369 | def chunk_list(d, n):
370 | return [d[x:x + n] for x in range(0, len(d), n)]
371 |
372 | def tor_broadcast_tx(txhex, tor_hostport, network, rpc, logger):
373 | CONNECTION_THREADS = 8
374 | CONNECTION_ATTEMPTS_PER_THREAD = 10
375 |
376 | required_address_count = CONNECTION_ATTEMPTS_PER_THREAD * CONNECTION_THREADS
377 | node_addrs_witness = []
378 | while True:
379 | try:
380 | new_node_addrs = rpc.call("getnodeaddresses",
381 | [3*required_address_count//2])
382 | except JsonRpcError as e:
383 | logger.debug(repr(e))
384 | logger.error("Bitcoin Core v0.18.0 or higher is required "
385 | "to broadcast through Tor")
386 | return False
387 | node_addrs_witness.extend(
388 | [a for a in new_node_addrs if a["services"] & NODE_WITNESS]
389 | )
390 | logger.debug("len(new_node_addrs) = " + str(len(new_node_addrs)) +
391 | " len(node_addrs_witness) = " + str(len(node_addrs_witness)))
392 | if len(node_addrs_witness) > required_address_count:
393 | break
394 | node_addrs_chunks = chunk_list(
395 | node_addrs_witness[:required_address_count],
396 | CONNECTION_ATTEMPTS_PER_THREAD
397 | )
398 | notify_queue = queue.Queue()
399 | start_height = rpc.call("getblockcount", [])
400 | for node_addrs in node_addrs_chunks:
401 | t = threading.Thread(target=broadcaster_thread,
402 | args=(txhex, node_addrs, tor_hostport, network, logger,
403 | start_height, notify_queue),
404 | daemon=True)
405 | t.start()
406 | try:
407 | success = notify_queue.get(block=True, timeout=20)
408 | except queue.Empty:
409 | logger.debug("Timed out getting notification for broadcasting "
410 | + "transaction")
411 | #the threads will maybe still continue to try broadcasting even
412 | # after this timeout
413 | #could time out at 20 seconds for any legitimate reason, tor is slow
414 | # so no point failing, this timeout is just so the user doesnt have
415 | # to stare at a seemingly-frozen dialog
416 | success = True
417 | return success
418 |
--------------------------------------------------------------------------------
/electrumpersonalserver/server/socks.py:
--------------------------------------------------------------------------------
1 | """SocksiPy - Python SOCKS module.
2 | Version 1.00
3 |
4 | Copyright 2006 Dan-Haim. All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 | 3. Neither the name of Dan Haim nor the names of his contributors may be used
14 | to endorse or promote products derived from this software without specific
15 | prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
18 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
20 | EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
25 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
26 |
27 |
28 | This module provides a standard socket-like interface for Python
29 | for tunneling connections through SOCKS proxies.
30 |
31 | """
32 |
33 | import socket
34 | import struct
35 | import random
36 |
37 | PROXY_TYPE_SOCKS4 = 1
38 | PROXY_TYPE_SOCKS5 = 2
39 | PROXY_TYPE_HTTP = 3
40 |
41 | _defaultproxy = None
42 | _orgsocket = socket.socket
43 |
44 |
45 | class ProxyError(IOError):
46 | def __init__(self, value):
47 | self.value = value
48 |
49 | def __str__(self):
50 | return repr(self.value)
51 |
52 |
53 | class GeneralProxyError(ProxyError):
54 | def __init__(self, value):
55 | self.value = value
56 |
57 | def __str__(self):
58 | return repr(self.value)
59 |
60 |
61 | class Socks5AuthError(ProxyError):
62 | def __init__(self, value):
63 | self.value = value
64 |
65 | def __str__(self):
66 | return repr(self.value)
67 |
68 |
69 | class Socks5Error(ProxyError):
70 | def __init__(self, value):
71 | self.value = value
72 |
73 | def __str__(self):
74 | return repr(self.value)
75 |
76 |
77 | class Socks4Error(ProxyError):
78 | def __init__(self, value):
79 | self.value = value
80 |
81 | def __str__(self):
82 | return repr(self.value)
83 |
84 |
85 | class HTTPError(ProxyError):
86 | def __init__(self, value):
87 | self.value = value
88 |
89 | def __str__(self):
90 | return repr(self.value)
91 |
92 |
93 | _generalerrors = ("success", "invalid data", "not connected", "not available",
94 | "bad proxy type", "bad input")
95 |
96 | _socks5errors = ("succeeded", "general SOCKS server failure",
97 | "connection not allowed by ruleset", "Network unreachable",
98 | "Host unreachable", "Connection refused", "TTL expired",
99 | "Command not supported", "Address type not supported",
100 | "Unknown error")
101 |
102 | _socks5autherrors = ("succeeded", "authentication is required",
103 | "all offered authentication methods were rejected",
104 | "unknown username or invalid password", "unknown error")
105 |
106 | _socks4errors = (
107 | "request granted", "request rejected or failed",
108 | "request rejected because SOCKS server cannot connect to identd on the client",
109 | "request rejected because the client program and identd report different user-ids",
110 | "unknown error")
111 |
112 |
113 | def setdefaultproxy(proxytype=None,
114 | addr=None,
115 | port=None,
116 | rdns=True,
117 | username=str(random.randrange(10000000, 99999999)),
118 | password=str(random.randrange(10000000, 99999999))):
119 | """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
120 | Sets a default proxy which all further socksocket objects will use,
121 | unless explicitly changed.
122 | """
123 | global _defaultproxy
124 | _defaultproxy = (proxytype, addr, port, rdns, username, password)
125 |
126 |
127 | class socksocket(socket.socket):
128 | """socksocket([family[, type[, proto]]]) -> socket object
129 |
130 | Open a SOCKS enabled socket. The parameters are the same as
131 | those of the standard socket init. In order for SOCKS to work,
132 | you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
133 | """
134 |
135 | def __init__(self,
136 | family=socket.AF_INET,
137 | type=socket.SOCK_STREAM,
138 | proto=0,
139 | _sock=None):
140 | _orgsocket.__init__(self, family, type, proto, _sock)
141 | if _defaultproxy is not None:
142 | self.__proxy = _defaultproxy
143 | else:
144 | self.__proxy = (None, None, None, None, None, None)
145 | self.__proxysockname = None
146 | self.__proxypeername = None
147 |
148 | def __recvall(self, bytes):
149 | """__recvall(bytes) -> data
150 | Receive EXACTLY the number of bytes requested from the socket.
151 | Blocks until the required number of bytes have been received.
152 | """
153 | data = b''
154 | while len(data) < bytes:
155 | data = data + self.recv(bytes - len(data))
156 | return data
157 |
158 | def setproxy(self,
159 | proxytype=None,
160 | addr=None,
161 | port=None,
162 | rdns=True,
163 | username=None,
164 | password=None):
165 | """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
166 | Sets the proxy to be used.
167 | proxytype - The type of the proxy to be used. Three types
168 | are supported: PROXY_TYPE_SOCKS4 (including socks4a),
169 | PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
170 | addr - The address of the server (IP or DNS).
171 | port - The port of the server. Defaults to 1080 for SOCKS
172 | servers and 8080 for HTTP proxy servers.
173 | rdns - Should DNS queries be preformed on the remote side
174 | (rather than the local side). The default is True.
175 | Note: This has no effect with SOCKS4 servers.
176 | username - Username to authenticate with to the server.
177 | The default is no authentication.
178 | password - Password to authenticate with to the server.
179 | Only relevant when username is also provided.
180 | """
181 | self.__proxy = (proxytype, addr, port, rdns, username, password)
182 |
183 | def __negotiatesocks5(self, destaddr, destport):
184 | """__negotiatesocks5(self,destaddr,destport)
185 | Negotiates a connection through a SOCKS5 server.
186 | """
187 | # First we'll send the authentication packages we support.
188 | if (self.__proxy[4] is not None) and (self.__proxy[5] is not None):
189 | # The username/password details were supplied to the
190 | # setproxy method so we support the USERNAME/PASSWORD
191 | # authentication (in addition to the standard none).
192 | self.sendall(b'\x05\x02\x00\x02')
193 | else:
194 | # No username/password were entered, therefore we
195 | # only support connections with no authentication.
196 | self.sendall(b'\x05\x01\x00')
197 | # We'll receive the server's response to determine which
198 | # method was selected
199 | chosenauth = self.__recvall(2)
200 | if chosenauth[0:1] != b"\x05":
201 | self.close()
202 | raise GeneralProxyError((1, _generalerrors[1]))
203 | # Check the chosen authentication method
204 | if chosenauth[1:2] == b"\x00":
205 | # No authentication is required
206 | pass
207 | elif chosenauth[1:2] == b"\x02":
208 | # Okay, we need to perform a basic username/password
209 | # authentication.
210 | self.sendall(b'\x01' + bytes([len(self.__proxy[4])]) + self.__proxy[4].encode() +
211 | bytes([len(self.__proxy[5])]) + self.__proxy[5].encode())
212 | authstat = self.__recvall(2)
213 | if authstat[0:1] != b"\x01":
214 | # Bad response
215 | self.close()
216 | raise GeneralProxyError((1, _generalerrors[1]))
217 | if authstat[1:2] != b"\x00":
218 | # Authentication failed
219 | self.close()
220 | raise Socks5AuthError((3, _socks5autherrors[3]))
221 | # Authentication succeeded
222 | else:
223 | # Reaching here is always bad
224 | self.close()
225 | if chosenauth[1:2] == b"\xFF":
226 | raise Socks5AuthError((2, _socks5autherrors[2]))
227 | else:
228 | raise GeneralProxyError((1, _generalerrors[1]))
229 | # Now we can request the actual connection
230 | req = b"\x05\x01\x00"
231 | # If the given destination address is an IP address, we'll
232 | # use the IPv4 address request even if remote resolving was specified.
233 | try:
234 | ipaddr = socket.inet_aton(destaddr)
235 | req = req + b"\x01" + ipaddr
236 | except socket.error:
237 | # Well it's not an IP number, so it's probably a DNS name.
238 | if self.__proxy[3]:
239 | # Resolve remotely
240 | ipaddr = None
241 | req = req + b"\x03" + bytes([len(destaddr)]) + destaddr.encode()
242 | else:
243 | # Resolve locally
244 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
245 | req = req + b"\x01" + ipaddr
246 | req += struct.pack(">H", destport)
247 | self.sendall(req)
248 | # Get the response
249 | resp = self.__recvall(4)
250 | if resp[0:1] != b"\x05":
251 | self.close()
252 | raise GeneralProxyError((1, _generalerrors[1]))
253 | elif resp[1:2] != b"\x00":
254 | # Connection failed
255 | self.close()
256 | raise Socks5Error(_socks5errors[min(9, ord(resp[1:2]))])
257 | # Get the bound address/port
258 | elif resp[3:4] == b"\x01":
259 | boundaddr = self.__recvall(4)
260 | elif resp[3:4] == b"\x03":
261 | resp = resp + self.recv(1)
262 | boundaddr = self.__recvall(resp[4:5])
263 | else:
264 | self.close()
265 | raise GeneralProxyError((1, _generalerrors[1]))
266 | boundport = struct.unpack(">H", self.__recvall(2))[0]
267 | self.__proxysockname = (boundaddr, boundport)
268 | if ipaddr is not None:
269 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
270 | else:
271 | self.__proxypeername = (destaddr, destport)
272 |
273 | def getproxysockname(self):
274 | """getsockname() -> address info
275 | Returns the bound IP address and port number at the proxy.
276 | """
277 | return self.__proxysockname
278 |
279 | def getproxypeername(self):
280 | """getproxypeername() -> address info
281 | Returns the IP and port number of the proxy.
282 | """
283 | return _orgsocket.getpeername(self)
284 |
285 | def getpeername(self):
286 | """getpeername() -> address info
287 | Returns the IP address and port number of the destination
288 | machine (note: getproxypeername returns the proxy)
289 | """
290 | return self.__proxypeername
291 |
292 | def __negotiatesocks4(self, destaddr, destport):
293 | """__negotiatesocks4(self,destaddr,destport)
294 | Negotiates a connection through a SOCKS4 server.
295 | """
296 | # Check if the destination address provided is an IP address
297 | rmtrslv = False
298 | try:
299 | ipaddr = socket.inet_aton(destaddr)
300 | except socket.error:
301 | # It's a DNS name. Check where it should be resolved.
302 | if self.__proxy[3]:
303 | ipaddr = b"\x00\x00\x00\x01"
304 | rmtrslv = True
305 | else:
306 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
307 | # Construct the request packet
308 | req = b"\x04\x01" + struct.pack(">H", destport) + ipaddr
309 | # The username parameter is considered userid for SOCKS4
310 | if self.__proxy[4] is not None:
311 | req = req + self.__proxy[4].encode()
312 | req += b"\x00"
313 | # DNS name if remote resolving is required
314 | # NOTE: This is actually an extension to the SOCKS4 protocol
315 | # called SOCKS4A and may not be supported in all cases.
316 | if rmtrslv:
317 | req = req + destaddr + b"\x00"
318 | self.sendall(req)
319 | # Get the response from the server
320 | resp = self.__recvall(8)
321 | if resp[0:1] != b"\x00":
322 | # Bad data
323 | self.close()
324 | raise GeneralProxyError((1, _generalerrors[1]))
325 | if resp[1:2] != b"\x5A":
326 | # Server returned an error
327 | self.close()
328 | if ord(resp[1:2]) in (91, 92, 93):
329 | self.close()
330 | raise Socks4Error((ord(resp[1]), _socks4errors[ord(resp[1:2]) -
331 | 90]))
332 | else:
333 | raise Socks4Error((94, _socks4errors[4]))
334 | # Get the bound address/port
335 | self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(
336 | ">H", resp[2:4])[0])
337 | if rmtrslv is not None:
338 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
339 | else:
340 | self.__proxypeername = (destaddr, destport)
341 |
342 | def __negotiatehttp(self, destaddr, destport):
343 | """__negotiatehttp(self,destaddr,destport)
344 | Negotiates a connection through an HTTP server.
345 | """
346 | # If we need to resolve locally, we do this now
347 | if not self.__proxy[3]:
348 | addr = socket.gethostbyname(destaddr)
349 | else:
350 | addr = destaddr
351 | self.sendall("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" +
352 | "Host: " + destaddr + "\r\n\r\n")
353 | # We read the response until we get the string "\r\n\r\n"
354 | resp = self.recv(1)
355 | while resp.find(b"\r\n\r\n") == -1:
356 | resp = resp + self.recv(1)
357 | # We just need the first line to check if the connection
358 | # was successful
359 | statusline = resp.splitlines()[0].split(b" ", 2)
360 | if statusline[0] not in ("HTTP/1.0", "HTTP/1.1"):
361 | self.close()
362 | raise GeneralProxyError((1, _generalerrors[1]))
363 | try:
364 | statuscode = int(statusline[1])
365 | except ValueError:
366 | self.close()
367 | raise GeneralProxyError((1, _generalerrors[1]))
368 | if statuscode != 200:
369 | self.close()
370 | raise HTTPError((statuscode, statusline[2]))
371 | self.__proxysockname = ("0.0.0.0", 0)
372 | self.__proxypeername = (addr, destport)
373 |
374 | def connect(self, destpair):
375 | """connect(self,despair)
376 | Connects to the specified destination through a proxy.
377 | destpar - A tuple of the IP/DNS address and the port number.
378 | (identical to socket's connect).
379 | To select the proxy server use setproxy().
380 | """
381 | # Do a minimal input check first
382 | if (type(destpair) in
383 | (list, tuple) == False) or (len(destpair) < 2) or (
384 | type(destpair[0]) != str) or (type(destpair[1]) != int):
385 | raise GeneralProxyError((5, _generalerrors[5]))
386 | if self.__proxy[0] == PROXY_TYPE_SOCKS5:
387 | if self.__proxy[2] is not None:
388 | portnum = self.__proxy[2]
389 | else:
390 | portnum = 1080
391 | _orgsocket.connect(self, (self.__proxy[1], portnum))
392 | self.__negotiatesocks5(destpair[0], destpair[1])
393 | elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
394 | if self.__proxy[2] is not None:
395 | portnum = self.__proxy[2]
396 | else:
397 | portnum = 1080
398 | _orgsocket.connect(self, (self.__proxy[1], portnum))
399 | self.__negotiatesocks4(destpair[0], destpair[1])
400 | elif self.__proxy[0] == PROXY_TYPE_HTTP:
401 | if self.__proxy[2] is not None:
402 | portnum = self.__proxy[2]
403 | else:
404 | portnum = 8080
405 | _orgsocket.connect(self, (self.__proxy[1], portnum))
406 | self.__negotiatehttp(destpair[0], destpair[1])
407 | elif self.__proxy[0] is None:
408 | _orgsocket.connect(self, (destpair[0], destpair[1]))
409 | else:
410 | raise GeneralProxyError((4, _generalerrors[4]))
411 |
--------------------------------------------------------------------------------
/release-notes:
--------------------------------------------------------------------------------
1 | # Release v0.2.4 (16th June 2022)
2 |
3 | New release, thanks to contributions by andrewtoth and theStack
4 | And thanks to everyone else who contributed via discussion and donations
5 |
6 | * Fixed crash caused by deprecated RPC in Bitcoin Core 23.0
7 | * Added signet support
8 |
9 | # Release v0.2.3 (9th March 2022)
10 |
11 | New release, thanks to contributions by andrewtoth and federicociro
12 | And thanks to everyone else who contributed via discussion and donations
13 |
14 | * Fixed crash caused by deprecated RPC in Bitcoin Core 22.0
15 |
16 | # Release v0.2.2 (29th May 2021)
17 |
18 | New release, thanks to contributions by Liongrass, Talkless and parazyd
19 | And thanks to everyone else who contributed via discussion and donations
20 |
21 | * Rewrite mempool handling to always be responsive. Previously the server
22 | would massively lag if the mempool was large, so most users just disabled
23 | the mempool histogram. With this update that is no longer an issue.
24 | * If Electrum Personal Server fails at startup it will now return a non-zero
25 | error code, making it more usable with automated scripts like systemd.
26 | * Client will now warn user if tor broadcasting fails because tor is not
27 | accessible.
28 | * Various optimizations and bug fixes.
29 |
30 | # Release v0.2.1.1 (9th June 2020)
31 |
32 | Bugfix release. Thanks to everyone who reported the bug and jmacxx who wrote
33 | the pull request to fix it. And thanks to everyone who contributed in general
34 | to electrum personal server.
35 |
36 | * Fixed bug where the server would crash in certain conditions. Instead the
37 | server should have caught the error and continued.
38 |
39 | # Release v0.2.1 (4th June 2020)
40 |
41 | New release, thanks to contributions by DriftwoodPalace, m-schmoock and wiredcheetah
42 | And thanks to everyone else who contributed via discussion and donations
43 |
44 | * Massive speedup to startup time and initial import of addresses. This is done
45 | using the descriptor wallets feature of Bitcoin Core 0.20. The speedup is
46 | very helpful when running Electrum Personal Server on low powered devices
47 | such as the raspberry pi
48 | * Close the connection to client if it requests an unknown address or if the
49 | connection to the Bitcoin node is lost. The user will see a red dot in
50 | Electrum indicating that something is wrong which should prompt them to fix.
51 | * Increase default polling interval to make the server more responsive to new
52 | transactions and confirmations
53 | * Reduce spam in the debug log and info log
54 | * Various other tweaks and bug fixes
55 |
56 | # Release v0.2.0 (5th December 2019)
57 |
58 | New release, thanks to code contributions by suvayu, andrewtoth and Sosthene00
59 | And thanks to everyone else who contributed via discussion and donations
60 |
61 | * Implemented tor broadcasting of transactions, which happens by default if tor
62 | is running on the same machine.
63 | * Also check that the last address of each master public key has been imported,
64 | along with the first three.
65 | * Add bandwidth usage per day and blockchain size to the server banner
66 | * Support using `vsize` instead of `size` for the mempool calculation, which is
67 | the correct behaviour for Bitcoin Core 0.19
68 | * Allow rescan date to also be passed via CLI args. Wait for any rescanning to
69 | finish on startup. This allows Electrum Personal Server to be more easily
70 | used with scripting.
71 | * Various other bugfixes
72 |
73 | # Release v0.1.7 (26th April 2019)
74 |
75 | New release, thanks to code contributions by suvayu and andrewtoth
76 | And thanks to everyone else who contributed via discussion and donations
77 |
78 | * If pruning is enabled and block is not available then send dummy merkle
79 | proof, which Electrum will accept if run with the command line
80 | flag --skipmerklecheck
81 | * Added option to allow broadcasting unconfirmed transactions via any
82 | system call, for example it could be a shell script which broadcasts
83 | via SMS or radio.
84 | * Added option which allows disabling the mempool histogram feature
85 | which is useful on low-powered devices when the mempool is large.
86 | * Deprecated electrum-personal-server-rescan script in favour of
87 | electrum-personal-server --rescan
88 | * Releases will now also be packaged as windows binaries using pyinstaller.
89 | * No longer adds orphaned coinbase txes as unconfirmed.
90 | * Fix bug involving transactions with unconfirmed inputs.
91 | * Various other bugfixes
92 |
93 | # Release v0.1.6 - (15th November 2018)
94 |
95 | New release, thanks to code contributions by suvayu and andrewtoth
96 | And thanks to everyone else who contributed to issues and discussion
97 |
98 | * Made installable with pip, thanks to suvayu
99 | * Fix bug where coinbase transactions would be ignored, thanks to andrewtoth
100 | * Support Electrum protocol version 1.4
101 | * Support blockchain.transaction.id_from_pos which is necessary for
102 | Lightning support in Electrum
103 | * Increase default initial_import_count to 1000
104 | * Added or clarified various error and info messages
105 | * Disabled transaction broadcasting when blocksonly is enabled for privacy
106 | * Fixed various small bugs
107 |
108 |
109 | # Release v0.1.5 - (7th September 2018)
110 |
111 | Bugfix release
112 |
113 | * Fixed crash bug caused by behavour of getaddressesbylabel
114 |
115 |
116 | # Release v0.1.4 - (5th September 2018)
117 |
118 | * Used 127.0.0.1 instead of localhost to help improve windows support
119 | * Fixed crash bug if the client requests an out-of-range header
120 | * Supported Bitcoin Core 0.17 which deprecates accounts
121 |
122 |
123 | # Release v0.1.3 - (4th July 2018)
124 |
125 | Bugfix release, mainly to correctly support Electrum 3.2
126 |
127 | * Added support for raw block headers
128 | * Implemented protocol method `blockchain.block.headers`
129 | * Make the address status of a empty address be None
130 | * Fixed bug involving rare situation where the result of the listtransactions
131 | RPC call did not have an `address` field
132 |
133 |
134 | # Release v0.1.2 - (30th June 2018)
135 |
136 | * Added support for mempool histogram feature
137 | * Handles conflicted transactions, for when a chain reorg happens
138 | * Added IP address whitelisting feature
139 | * Bugfix when Electrum requests block headers out of range
140 | * Bugfix when listtransactions has more than 1000 entries
141 | * Added many more tests, which now use py.test
142 | * Added regtest support
143 |
144 |
145 | # Release v0.1.1 - (1st April 2018)
146 |
147 | Bugfix release, thanks to
148 |
149 | * Added option to manually configure rpc_user and rpc_password, instead of using
150 | the .cookie file.
151 | * Made json-rpc error messages have more detail.
152 | * Added method for user to configure Electrum Personal Server's current working
153 | directory, which is useful for running it from systemd or another automated
154 | tool.
155 | * Updated readme file to add information that tripped people up.
156 | * Now handles conflicted transactions.
157 |
158 |
159 | # Beta release v0.1 - (29th Mar 2018)
160 |
161 | Released first beta version.
162 |
163 | * Merkle proofs supported using bitcoind's `gettxoutproof` RPC call.
164 | * Deterministic wallets implemented which support all Electrum master public
165 | key formats.
166 | * Created rescan script which allows rescanning from a given block height
167 | instead of scanning the entire blockchain. Also allows the user to input a
168 | dd/mm/yyyy timestamp, which is converted to a block height (with 2 weeks
169 | safety) to rescan from there.
170 | * Automated tests created for merkle proofs, deterministic wallets and
171 | monitoring of transactions.
172 | * SSL server socket is used and a default SSL certificate is included in the
173 | repository, which users can replace with their own.
174 | * No longer depends on pybitcointools' transaction code. That package is only
175 | used for bip32 support for deterministic wallets. Bech32 addresses now
176 | supported.
177 | * RPC auth details can be obtained from the .cookie file.
178 | * Bitcoin Core's multi-wallet feature supported.
179 |
180 |
181 | # Alpha release - (8th Feb 2018)
182 |
183 | Released first alpha version which builds address history from bitcoin'd
184 | wallet, monitors addresses for new transactions and accepts connections from
185 | Electrum wallet.
186 |
187 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | test=pytest
3 |
4 | [tool:pytest]
5 | addopts = --pdb
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="electrum-personal-server",
5 | version="0.2.4",
6 | description="Electrum Personal Server",
7 | author="Chris Belcher",
8 | license="MIT",
9 | include_package_data=True,
10 | packages=find_packages(exclude=["tests"]),
11 | tests_require=["pytest"],
12 | entry_points={
13 | "console_scripts": [
14 | "electrum-personal-server = electrumpersonalserver.server.common:main",
15 | ]
16 | },
17 | package_data={"electrumpersonalserver": ["certs/*"]},
18 | data_files=[
19 | ("share/doc/electrum-personal-server", ["README.md",
20 | "config.ini_sample"]),
21 | ],
22 | )
23 |
--------------------------------------------------------------------------------
/test/test_electrum_protocol.py:
--------------------------------------------------------------------------------
1 |
2 | import pytest
3 | import logging
4 | import json
5 |
6 | from electrumpersonalserver.server import (
7 | TransactionMonitor,
8 | JsonRpcError,
9 | ElectrumProtocol,
10 | get_block_header,
11 | get_current_header,
12 | get_block_headers_hex,
13 | JsonRpcError,
14 | get_status_electrum
15 | )
16 |
17 | logger = logging.getLogger('ELECTRUMPERSONALSERVER-TEST')
18 | logger.setLevel(logging.DEBUG)
19 |
20 | DUMMY_JSONRPC_BLOCKCHAIN_HEIGHT = 100000
21 |
22 | def get_dummy_hash_from_height(height):
23 | if height == 0:
24 | return "00"*32
25 | return str(height) + "a"*(64 - len(str(height)))
26 |
27 | def get_height_from_dummy_hash(hhash):
28 | if hhash == "00"*32:
29 | return 0
30 | return int(hhash[:hhash.index("a")])
31 |
32 | class DummyJsonRpc(object):
33 | def __init__(self):
34 | self.calls = {}
35 | self.blockchain_height = DUMMY_JSONRPC_BLOCKCHAIN_HEIGHT
36 |
37 | def call(self, method, params):
38 | if method not in self.calls:
39 | self.calls[method] = [0, []]
40 | self.calls[method][0] += 1
41 | self.calls[method][1].append(params)
42 | if method == "getbestblockhash":
43 | return get_dummy_hash_from_height(self.blockchain_height)
44 | elif method == "getblockhash":
45 | height = params[0]
46 | if height > self.blockchain_height:
47 | raise JsonRpcError()
48 | return get_dummy_hash_from_height(height)
49 | elif method == "getblockheader":
50 | blockhash = params[0]
51 | height = get_height_from_dummy_hash(blockhash)
52 | header = {
53 | "hash": blockhash,
54 | "confirmations": self.blockchain_height - height + 1,
55 | "height": height,
56 | "version": 536870912,
57 | "versionHex": "20000000",
58 | "merkleroot": "aa"*32,
59 | "time": height*100,
60 | "mediantime": height*100,
61 | "nonce": 1,
62 | "bits": "207fffff",
63 | "difficulty": 4.656542373906925e-10,
64 | "chainwork": "000000000000000000000000000000000000000000000"
65 | + "00000000000000000da",
66 | "nTx": 1,
67 | }
68 | if height > 1:
69 | header["previousblockhash"] = get_dummy_hash_from_height(
70 | height - 1)
71 | elif height == 1:
72 | header["previousblockhash"] = "00"*32 #genesis block
73 | elif height == 0:
74 | pass #no prevblock for genesis
75 | else:
76 | assert 0
77 | if height < self.blockchain_height:
78 | header["nextblockhash"] = get_dummy_hash_from_height(height + 1)
79 | return header
80 | elif method == "gettransaction":
81 | for t in self.txlist:
82 | if t["txid"] == params[0]:
83 | return t
84 | raise JsonRpcError()
85 | else:
86 | raise ValueError("unknown method in dummy jsonrpc")
87 |
88 | def test_get_block_header():
89 | rpc = DummyJsonRpc()
90 | for height in [0, 1000]:
91 | for raw in [True, False]:
92 | blockhash = rpc.call("getblockhash", [height])
93 | ret = get_block_header(rpc, blockhash, raw)
94 | if raw:
95 | assert type(ret) == dict
96 | assert "hex" in ret
97 | assert "height" in ret
98 | assert len(ret["hex"]) == 160
99 | else:
100 | assert type(ret) == dict
101 | assert len(ret) == 7
102 |
103 | def test_get_current_header():
104 | rpc = DummyJsonRpc()
105 | for raw in [True, False]:
106 | ret = get_current_header(rpc, raw)
107 | assert type(ret[0]) == str
108 | assert len(ret[0]) == 64
109 | if raw:
110 | assert type(ret[1]) == dict
111 | assert "hex" in ret[1]
112 | assert "height" in ret[1]
113 | assert len(ret[1]["hex"]) == 160
114 | else:
115 | assert type(ret[1]) == dict
116 | assert len(ret[1]) == 7
117 |
118 | @pytest.mark.parametrize(
119 | "start_height, count",
120 | [(100, 200),
121 | (DUMMY_JSONRPC_BLOCKCHAIN_HEIGHT + 10, 5),
122 | (DUMMY_JSONRPC_BLOCKCHAIN_HEIGHT - 10, 15),
123 | (0, 250)
124 | ]
125 | )
126 | def test_get_block_headers_hex(start_height, count):
127 | rpc = DummyJsonRpc()
128 | ret = get_block_headers_hex(rpc, start_height, count)
129 | print("start_height=" + str(start_height) + " count=" + str(count))
130 | assert len(ret) == 2
131 | available_blocks = -min(0, start_height - DUMMY_JSONRPC_BLOCKCHAIN_HEIGHT
132 | - 1)
133 | expected_count = min(available_blocks, count)
134 | assert len(ret[0]) == expected_count*80*2 #80 bytes/header, 2 chars/byte
135 | assert ret[1] == expected_count
136 |
137 | @pytest.mark.parametrize(
138 | "invalid_json_query",
139 | [
140 | {"valid-json-no-method": 5}
141 | ]
142 | )
143 | def test_invalid_json_query_line(invalid_json_query):
144 | protocol = ElectrumProtocol(None, None, logger, None, None, None)
145 | with pytest.raises(IOError) as e:
146 | protocol.handle_query(invalid_json_query)
147 |
148 | def create_electrum_protocol_instance(broadcast_method="own-node",
149 | tor_hostport=("127.0.0.1", 9050),
150 | disable_mempool_fee_histogram=False):
151 | protocol = ElectrumProtocol(DummyJsonRpc(), DummyTransactionMonitor(),
152 | logger, broadcast_method, tor_hostport, disable_mempool_fee_histogram)
153 | sent_replies = []
154 | protocol.set_send_reply_fun(lambda l: sent_replies.append(l))
155 | assert len(sent_replies) == 0
156 | return protocol, sent_replies
157 |
158 | def dummy_script_hash_to_history(scrhash):
159 | index = int(scrhash[:scrhash.index("s")])
160 | tx_count = (index+2) % 5
161 | height = 500
162 | return [(index_to_dummy_txid(i), height) for i in range(tx_count)]
163 |
164 | def index_to_dummy_script_hash(index):
165 | return str(index) + "s"*(64 - len(str(index)))
166 |
167 | def index_to_dummy_txid(index):
168 | return str(index) + "t"*(64 - len(str(index)))
169 |
170 | def dummy_txid_to_dummy_tx(txid):
171 | return txid[::-1] * 6
172 |
173 | class DummyTransactionMonitor(object):
174 | def __init__(self):
175 | self.deterministic_wallets = list(range(5))
176 | self.address_history = list(range(5))
177 | self.subscribed_addresses = []
178 | self.history_hashes = {}
179 |
180 | def get_electrum_history_hash(self, scrhash):
181 | history = dummy_script_hash_to_history(scrhash)
182 | hhash = get_status_electrum(history)
183 | self.history_hashes[scrhash] = history
184 | return hhash
185 |
186 | def get_electrum_history(self, scrhash):
187 | return self.history_hashes[scrhash]
188 |
189 | def unsubscribe_all_addresses(self):
190 | self.subscribed_addresses = []
191 |
192 | def subscribe_address(self, scrhash):
193 | self.subscribed_addresses.append(scrhash)
194 | return True
195 |
196 | def get_address_balance(self, scrhash):
197 | pass
198 |
199 | def test_script_hash_sync():
200 | protocol, sent_replies = create_electrum_protocol_instance()
201 | scrhash_index = 0
202 | scrhash = index_to_dummy_script_hash(scrhash_index)
203 | protocol.handle_query({"method": "blockchain.scripthash.subscribe",
204 | "params": [scrhash], "id": 0})
205 | assert len(sent_replies) == 1
206 | assert len(protocol.txmonitor.subscribed_addresses) == 1
207 | assert protocol.txmonitor.subscribed_addresses[0] == scrhash
208 | assert len(sent_replies) == 1
209 | assert len(sent_replies[0]["result"]) == 64
210 | history_hash = sent_replies[0]["result"]
211 |
212 | protocol.handle_query({"method": "blockchain.scripthash.get_history",
213 | "params": [scrhash], "id": 0})
214 | assert len(sent_replies) == 2
215 | assert get_status_electrum(sent_replies[1]["result"]) == history_hash
216 |
217 | #updated scripthash but actually nothing changed, history_hash unchanged
218 | protocol.on_updated_scripthashes([scrhash])
219 | assert len(sent_replies) == 3
220 | assert sent_replies[2]["method"] == "blockchain.scripthash.subscribe"
221 | assert sent_replies[2]["params"][0] == scrhash
222 | assert sent_replies[2]["params"][1] == history_hash
223 |
224 | protocol.on_disconnect()
225 | assert len(protocol.txmonitor.subscribed_addresses) == 0
226 |
227 | def test_headers_subscribe():
228 | protocol, sent_replies = create_electrum_protocol_instance()
229 |
230 | protocol.handle_query({"method": "server.version", "params": ["test-code",
231 | 1.4], "id": 0}) #protocol version of 1.4 means only raw headers used
232 | assert len(sent_replies) == 1
233 |
234 | protocol.handle_query({"method": "blockchain.headers.subscribe", "params":
235 | [], "id": 0})
236 | assert len(sent_replies) == 2
237 | assert "height" in sent_replies[1]["result"]
238 | assert sent_replies[1]["result"]["height"] == protocol.rpc.blockchain_height
239 | assert "hex" in sent_replies[1]["result"]
240 | assert len(sent_replies[1]["result"]["hex"]) == 80*2 #80 b/header, 2 b/char
241 |
242 | protocol.rpc.blockchain_height += 1
243 | new_bestblockhash, header = get_current_header(protocol.rpc,
244 | protocol.are_headers_raw)
245 | protocol.on_blockchain_tip_updated(header)
246 | assert len(sent_replies) == 3
247 | assert "method" in sent_replies[2]
248 | assert sent_replies[2]["method"] == "blockchain.headers.subscribe"
249 | assert "params" in sent_replies[2]
250 | assert "height" in sent_replies[2]["params"][0]
251 | assert sent_replies[2]["params"][0]["height"]\
252 | == protocol.rpc.blockchain_height
253 | assert "hex" in sent_replies[2]["params"][0]
254 | assert len(sent_replies[2]["params"][0]["hex"]) == 80*2 #80 b/header, 2 b/c
255 |
256 | def test_server_ping():
257 | protocol, sent_replies = create_electrum_protocol_instance()
258 | idd = 1
259 | protocol.handle_query({"method": "server.ping", "id": idd})
260 | assert len(sent_replies) == 1
261 | assert sent_replies[0]["result"] == None
262 | assert sent_replies[0]["id"] == idd
263 |
264 | #test scripthash.subscribe, scripthash.get_history transaction.get
265 | # transaction.get_merkle
266 |
267 |
--------------------------------------------------------------------------------
/test/test_merkleproof.py:
--------------------------------------------------------------------------------
1 |
2 | import pytest
3 |
4 | from electrumpersonalserver.server import (
5 | convert_core_to_electrum_merkle_proof,
6 | hash_merkle_root
7 | )
8 |
9 | '''
10 | # code for printing out proofs not longer than 80 per line
11 | proof = ""
12 | def chunks(d, n):
13 | return [d[x:x + n] for x in range(0, len(d), n)]
14 | print("\" + \n\"".join(chunks(proof, 71)))
15 | '''
16 |
17 | @pytest.mark.parametrize(
18 | "proof",
19 | [
20 | #txcount 819, pos 5
21 | "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f000000000000000" +
22 | "04f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431acd0b07f" +
23 | "5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0fe098f6f9e090" +
24 | "f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815dc237f77118a45d7" +
25 | "5ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819198212216b5d62d75be5" +
26 | "9221ada17ba4fb2476b689cccd3be54732fd5630832a94f11fa3f0dafd6f904d43219e0" +
27 | "d7de110158446b5b598bd241f7b5df4da0ebc7d30e7748d487917b718df51c681174e6a" +
28 | "bab8042cc7c1c436221c098f06a56134f9247a812126d675d69c82ba1c715cfc0cde462" +
29 | "fd1fbe5dc87f6b8db2b9c060fcd59a20e7fe8e921c3676937a873ff88684f4be4d015f2" +
30 | "4f26af6d2cf78335e9218bcceba4507d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69" +
31 | "590fb0f5118ac937c02ccd47e9d90be78becd11ecf854d7d268eeb479b74d137278c0a5" +
32 | "017d29e90cd5b35a4680201824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb210" +
33 | "5f3d2e8bcc94d547214a9d03ff1600",
34 | #txcount 47, pos 9
35 | "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab80100000000000" +
36 | "01532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce05577c67" +
37 | "508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff2ba638d8d400" +
38 | "b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd990ce150b6e2dc9e9e" +
39 | "56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568fb117674ad23050e29311" +
40 | "97494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7a67ee03837319e098b43edad" +
41 | "3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38982e03bd292f5bd02f28137d343f" +
42 | "908c7d6417379afe8349a257af3ca1f74f623be6a416fe1aa96a8f259983f2cf32121bc" +
43 | "e203955a378b3b44f132ea6ab94c7829a6c3b360c9f8da8e74027701",
44 | #txcount 2582, pos 330
45 | "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb17800000000000000000" +
46 | "01032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd4078c771849" +
47 | "5a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d85e56491ed6d" +
48 | "204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3efb7773d457369c228" +
49 | "fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a3b221b47ea9c8e6f4870" +
50 | "7b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0fbcba4c741bb5c75cac58ca0" +
51 | "4161a7377d70f3fd19a3e248ed918c91709b49afd3760f89ed2fefbcc9c23447ccb40a2" +
52 | "be7aba22b07189b0bf90c62db48a9fe37227e12c7af8c1d4c22f9f223530dacdd5f3ad8" +
53 | "50ad4badf16cc24049a65334f59bf28c15cecda1a4cf3f2937bd70ee84f66569ce8ef95" +
54 | "1d50cca46d60337e6c697685b38ad217967bbe6801d03c44fcb808cd035be31888380a2" +
55 | "df1be14b6ff100de83cab0dce250e2b40ca3b47e8309f848646bee63b6185c176d84f15" +
56 | "46a482e7a65a87d1a2d0d5a2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4" +
57 | "ecaeea7d5988745fa421a8604a99857426957a2ccfa7cd8df145aa8293701989dd20750" +
58 | "5923fcb339843944ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74" +
59 | "c0bdd4b6046fb70100",
60 | #txcount 2861, pos 2860, last tx with many duplicated nodes down the tree
61 | "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e00000000000000000" +
62 | "0cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d86159" +
63 | "5ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29" +
64 | "f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc7" +
65 | "2df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974b" +
66 | "df42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290" +
67 | "bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f" +
68 | "171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6" +
69 | "c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07",
70 | #txcount 7, pos 6, duplicated entry in the last depth, at tx level
71 | "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a346200000000000" +
72 | "0af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb4" +
73 | "4dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f" +
74 | "888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22" +
75 | "719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d" +
76 | "8be0bc40135",
77 | #txcount 6, pos 5, duplicated entry in the last-but-one depth
78 | "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b0300000000000" +
79 | "0e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa1" +
80 | "4e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751" +
81 | "ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b5" +
82 | "8a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc" +
83 | "6edefea012d",
84 | #txcount 5, pos 4, duplicated on the last and second last depth
85 | "010000004d891de57908d89e9e5585648978d7650adc67e856c2d8c18c1800000000000" +
86 | "04746fd317bffecd4ffb320239caa06685bafe0c1b5463b24d636e45788796657843d1b" +
87 | "4d4c86041be68355c40500000002d8e26c89c46477f2407d866d2badbd98e43e732a670" +
88 | "e96001faf1744b27e5fdd018733d72e31a2d6a0d94f2a3b35fcc66fb110c40c5bbff82b" +
89 | "f87606553d541d011d",
90 | #txcount 2739, pos 0, coinbase tx
91 | "000000209f283da030c6e6d0ff5087a87c430d140ed6b4564fa34d00000000000000000" +
92 | "0ec1513723e3652c6b8e777c41eb267ad8dd2025e85228840f5cfca7ffe1fb331afff8a" +
93 | "5af8e961175e0f7691b30a00000df403e21a4751fbd52457f535378ac2dcf111199e9ea" +
94 | "6f78f6c2663cb99b58203438d8f3b26f7f2804668c1df7d394a4726363d4873b2d85b71" +
95 | "2e44cf4f5e4f33f22a8f3a1672846bd7c4570c668e6ee12befda23bfa3d0fcd30b1b079" +
96 | "19b01c40b1e31b6d34fcdbb99539d46eb97a3ae15386f1ab0f28ecacadd9fc3fa4ce49a" +
97 | "1a1839d815229f54036c8a3035d91e80e8dc127b62032b4e652550b4fc0aee0f6e85a14" +
98 | "307d85ed9dde62acff9a0f7e3b52370a10d6c83ec13a0b4a8fafe87af368a167d7e9b63" +
99 | "3b84b6ea65f1ce5e8ccc1840be0a4dab0099e25afccc7f2fdbda54cd65ecbac8d9a550c" +
100 | "108b4e18d3af59129d373fde4c80848858fd6f7fc1e27387a38833473ca8a47729fa6e1" +
101 | "cc14b584c14dad768108ff18cc6acdc9c31d32dc71c3c80856664a3fff870fe419a59aa" +
102 | "9033356590475d36086f0b3c0ece34c0f3756675c610fb980ff3363af6f9c0918a7c677" +
103 | "23371849de9c1026515c2900a80b3aee4f2625c8f48cd5eb967560ee8ebe58a8d41c331" +
104 | "f6d5199795735d4f0494bdf592d166fa291062733619f0f133605087365639de2d9d5d6" +
105 | "921f4b4204ff1f0000",
106 | #txcount 1, pos 0, coinbase tx in an empty block, tree with height 1
107 | "010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d0000000" +
108 | "0112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd66" +
109 | "49ffff001d1e2de5650100000001112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98" +
110 | "173ac9799a852fa39add30101",
111 | #txcount 2, pos 1, tree with height 2
112 | "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc30010000000" +
113 | "02793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b" +
114 | "4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa2" +
115 | "1aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a190" +
116 | "2360baa5e4328e0105"
117 | ]
118 | )
119 |
120 | def test_merkleproof(proof):
121 | try:
122 | electrum_proof = convert_core_to_electrum_merkle_proof(proof)
123 | #print(electrum_proof)
124 | implied_merkle_root = hash_merkle_root(
125 | electrum_proof["merkle"], electrum_proof["txid"],
126 | electrum_proof["pos"])
127 | assert implied_merkle_root == electrum_proof["merkleroot"]
128 | except ValueError:
129 | import traceback
130 | traceback.print_exc()
131 | assert 0
132 |
133 |
--------------------------------------------------------------------------------
/test/test_parse_mpks.py:
--------------------------------------------------------------------------------
1 |
2 | import pytest
3 |
4 | from electrumpersonalserver.server import parse_electrum_master_public_key
5 |
6 |
7 | @pytest.mark.parametrize(
8 | "bad_master_public_key",
9 | [
10 | "zpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" +
11 | "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #bad checksum
12 | "a tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
13 | "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" +
14 | "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" +
15 | "T74sszGaxaf64o5s", #unparsable m number
16 | "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" +
17 | "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ Vpub5fAqpSRkLmvXwqbuR61M" +
18 | "aKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHpdvrg2LHHXkKxSXBHNWNpZz9b1Vq" +
19 | "ADjmcCs3arSoxN3F3r", #inconsistent magic
20 | "e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d" +
21 | "5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442", #wrong length
22 | "e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d" +
23 | "5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442ZZ" #not hex
24 | ]
25 | )
26 |
27 | def test_parse_bad_mpk(bad_master_public_key):
28 | try:
29 | parse_electrum_master_public_key(bad_master_public_key, 5)
30 | raised_error = False
31 | except (ValueError, Exception):
32 | raised_error = True
33 | assert raised_error
34 |
--------------------------------------------------------------------------------