├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── channel.py ├── config.py ├── demo.py ├── jsonrpcproxy.py ├── lightning.py ├── lightningd.py ├── local.py ├── notify.py ├── pylintrc ├── requirements.txt ├── serverutil.py └── test ├── __init__.py ├── regnet.py ├── test_integration.py └── test_jsonrpcproxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | regnet 4 | 5 | bitcoind 6 | bitcoin-cli 7 | alphad 8 | alpha-cli 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.3" 5 | - "3.5.0b3" 6 | - "3.5-dev" 7 | - "nightly" 8 | - "3.2" 9 | # It would be nice to at least monitor 2.x. 10 | # But no tests currently get run. See #16. 11 | matrix: 12 | fast_finish: true 13 | allow_failures: 14 | - python: "2.6" 15 | - python: "2.7" 16 | - python: "3.2" 17 | - python: "3.3" 18 | - python: "3.5.0b3" 19 | - python: "3.5-dev" 20 | - python: "nightly" 21 | # command to install dependencies 22 | install: "pip install -r requirements.txt" 23 | before_script: 24 | # Download bitcoind 25 | - wget https://bitcoin.org/bin/bitcoin-core-0.11.0/bitcoin-0.11.0-linux64.tar.gz 26 | - tar -xzvf bitcoin-0.11.0-linux64.tar.gz 27 | - cp bitcoin-0.11.0/bin/bitcoind bitcoind 28 | # command to run tests 29 | script: 30 | - python -m unittest 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 HashPlex 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightning network node implementation 2 | ===================================== 3 | 4 | [![Build Status](https://travis-ci.org/hashplex/Lightning.svg)](https://travis-ci.org/hashplex/Lightning) 5 | 6 | Lightning 7 | --------- 8 | 9 | The initial paper on Lightning by Joseph Poon and Thaddeus Dryja: https://lightning.network/ 10 | 11 | Lightning is a trustless clearinghouse network based on micropayment channels and backed by Bitcoin. It makes fast, cheap microtransactions possible. 12 | 13 | This is an experimental implementation of a Lightning node. 14 | 15 | License 16 | ------- 17 | 18 | This code is released under the terms of the MIT license. See [LICENSE](LICENSE) for more 19 | information or see http://opensource.org/licenses/MIT. 20 | 21 | Overview 22 | -------- 23 | 24 | This is an implementation of a Lightning node. Its goal is to foster experimentation with the Lightning protocol by simultaneously providing a full stack implementation that can stand alone and providing sufficient modularity that the server, the micropayment channel protocol, the routing protocol, and the user interface can all be developed independently of each other. 25 | 26 | `demo.py`, described under Usage below, is a good place to experiment. 27 | 28 | `test/test_integration.py` is where I recommend you start reading, specifically `TestChannel.test_basic` and `TestLightning.test_payment`. It demonstrates intended usage of the project. 29 | 30 | Directory: 31 | - The server is split across `lightningd.py` and `serverutil.py`. 32 | - The micropayment channel protocol is implemented in `channel.py`. 33 | - The routing protocol is implemented in `lightning.py`. 34 | 35 | Docstrings at the top of `serverutil.py`, `channel.py`, and `lightning.py` describe the interface they expose. 36 | 37 | Usage 38 | ----- 39 | 40 | I develop on Ubuntu 14.04 with Python 3.4.0. 41 | Travis tests on Ubuntu 12.04 with Python 3.3+ 42 | 43 | - Grab a bitcoind 0.11.0 executable and put it in the directory. 44 | - Set up a virtualenv and install from `requirements.txt`. 45 | - Tests can be run as `python -m unittest`. 46 | - Run `python -i demo.py` to setup a regtest network and get proxies to all three nodes (Read demo.py for usage) 47 | 48 | Design 49 | ------ 50 | 51 | The lightning node is split into 4 pieces: the server, micropayment channels, and lightning routing, and the user interface. 52 | 53 | The server is responsible for talking to the user and to other nodes. It is currently split across 2 files, `lightningd.py` and `serverutil.py`. 54 | 55 | 1. `lightningd.py` is the body of the server, it sets up a Flask app and installs the channel interface, lightning interface, and user interface. The Flask dev server is used, configured to run with multiple processes. 56 | 2. `serverutil.py` is how the channel, lightning and user interfaces talk with the server. It contains authentication helpers as well as `api_factory`, which provides an API Blueprint object to attach before and after request hooks, and also a decorator which exposes functions to the RPC interface. JSON-RPC is currently used both for inter-node communication as well as user interaction, since JSON-RPC was easy and flexible to implement. 57 | 58 | Micropayment channel functionality resides in `channel.py`. It contains functions to open, update, and close channels. Communication is accomplished by RPC calls to other nodes. This module currently sets up its own sqlite database, but this should really be moved to the server. Channels are not currently secure or robust. A 2 of 2 multisig anchor is set up by mutual agreement. During operation and closing, commitment signatures are exchanged, which provides support for unilateral close. There is no support for revoking commitment transactions yet. There is also no support for HTLCs yet. Rusty has developed a secure protocol, and I am working on implementing it. 59 | 60 | Lightning routing functionality resides in `lightning.py`. It contains functions to maintain the routing table, and send payment over multiple hops. This module also currently sets up its own database, but this should really be moved to the server. The lightning module listens for a channel being opened, and propagates updates in the routing table to its peers. Currently routing does not handle a channel being closed. When money is sent, the next hop is determined from the routing table. Payment is sent to the next hop, and the next hop is requested to forward payment to the destination. The Lightning paper described how HTLCs could be used to secure this multi-hop payment. 61 | 62 | The user interface currently consists of RPC calls to the /local endpoint. It should be easy to stick a HTML wallet-like user interface on as well, and/or a lightning-qt could be developed. These GUIs would likely talk to lightningd over the aforementiond local RPC interface. 63 | 64 | This project is in its infancy. The current implementation is very naive (trusting, slow, unsecured). The next step is to write an implementation which removes these limitations. This project aims to be a testbed to facilitate experimentation with micropayment channels and routing in a fully realized system with integration tests able to validate the whole stack at once. 65 | 66 | Testing 67 | ------- 68 | 69 | Travis CI tests the project against all versions of Python it knows, currently Python 3.3+ are passing. 70 | 71 | `test/test_integration.py` currently contains an easy set of positive tests for micropayment channels and routing. More tests need to be written to demonstrate the holes in the current implementation. Specifically, I test that I can set up multiple micropayment channels, send and recieve money in them, spend my entire balance, send payment to a node multiple hops away, and close the channels. I also have a test (currently failing) for the case that Alice sends a revoked commitment transaction and then shuts up, in which case Bob should be able to take all the money in their channel. There is annother test (now passing) for unilateral close. More tests are needed for various other error cases. 72 | 73 | Code coverage is not yet set up, since the current problem is not having enough implementation rather than not enough tests. This should change. 74 | 75 | The project is currently linted with `pylint *.py` 76 | 77 | Other Work 78 | ---------- 79 | 80 | This project is founded on work by Joseph Poon and Thaddeus Dryja who wrote the initial Lightning paper. (https://lightning.network/) 81 | 82 | Rusty Russel has contributed several improvements to Lightning (http://ozlabs.org/~rusty/ln-deploy-draft-01.pdf), and is working on an implementation (https://github.com/ElementsProject/lightning). His implementation sidesteps questions about communication, persistence, user interface, and routing, all of which this project is designed to address. 83 | -------------------------------------------------------------------------------- /channel.py: -------------------------------------------------------------------------------- 1 | """Micropayment channel API for a lightning node. 2 | 3 | Interface: 4 | API -- the Blueprint returned by serverutil.api_factory 5 | 6 | CHANNEL_OPENED -- a blinker signal sent when a channel is opened. 7 | Arguments: 8 | - address -- the url of the counterparty 9 | 10 | init(conf) - Set up the database 11 | create(url, mymoney, theirmoney) 12 | - Open a channel with the node identified by url, 13 | where you can send mymoney satoshis, and recieve theirmoney satoshis. 14 | send(url, amount) 15 | - Update a channel by sending amount satoshis to the node at url. 16 | getbalance(url) 17 | - Return the number of satoshis you can send in the channel with url. 18 | close(url) 19 | - Close the channel with url. 20 | getcommitmenttransactions(url) 21 | - Return a list of the commitment transactions in a payment channel 22 | 23 | HTLC operation has not yet been defined. 24 | 25 | Error conditions have not yet been defined. 26 | 27 | Database: 28 | The schema is currently one row for each channel in table CHANNELS. 29 | address: url for the counterpary 30 | commitment: your commitment transaction 31 | """ 32 | 33 | from sqlalchemy import Column, Integer, String, LargeBinary 34 | from flask import g 35 | from blinker import Namespace 36 | from bitcoin.core import COutPoint, CMutableTxOut, CMutableTxIn 37 | from bitcoin.core import CMutableTransaction 38 | from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH 39 | from bitcoin.core.script import CScript, SignatureHash, SIGHASH_ALL 40 | from bitcoin.core.script import OP_CHECKMULTISIG, OP_PUBKEY 41 | from bitcoin.wallet import CBitcoinAddress 42 | import jsonrpcproxy 43 | from serverutil import api_factory 44 | from serverutil import database 45 | from serverutil import ImmutableSerializableType, Base58DataType 46 | 47 | API, REMOTE, Model = api_factory('channel') 48 | 49 | SIGNALS = Namespace() 50 | CHANNEL_OPENED = SIGNALS.signal('CHANNEL_OPENED') 51 | 52 | class AnchorScriptSig(object): 53 | """Class representing a scriptSig satisfying the anchor output. 54 | 55 | Uses OP_PUBKEY to hold the place of your signature. 56 | """ 57 | 58 | def __init__(self, my_index=0, sig=b'', redeem=b''): 59 | if my_index == b'': 60 | my_index = 0 61 | if my_index not in [0, 1]: 62 | raise Exception("Unknown index", my_index) 63 | self.my_index = my_index 64 | self.sig = sig 65 | self.redeem = CScript(redeem) 66 | 67 | @classmethod 68 | def from_script(cls, script): 69 | """Construct an AnchorScriptSig from a CScript.""" 70 | script = list(script) 71 | assert len(script) == 4 72 | if script[1] == OP_PUBKEY: 73 | return cls(0, script[2], script[3]) 74 | elif script[2] == OP_PUBKEY: 75 | return cls(1, script[1], script[3]) 76 | else: 77 | raise Exception("Could not find OP_PUBKEY") 78 | 79 | def to_script(self, sig=OP_PUBKEY): 80 | """Construct a CScript from an AnchorScriptSig.""" 81 | if self.my_index == 0: 82 | sig1, sig2 = sig, self.sig 83 | elif self.my_index == 1: 84 | sig1, sig2 = self.sig, sig 85 | else: 86 | raise Exception("Unknown index", self.my_index) 87 | return CScript([0, sig1, sig2, self.redeem]) 88 | 89 | class Channel(Model): 90 | """Model of a payment channel.""" 91 | 92 | __tablename__ = 'channels' 93 | 94 | address = Column(String, primary_key=True) 95 | anchor_point = Column(ImmutableSerializableType(COutPoint), 96 | unique=True, index=True) 97 | anchor_index = Column(Integer) 98 | their_sig = Column(LargeBinary) 99 | anchor_redeem = Column(LargeBinary) 100 | our_balance = Column(Integer) 101 | our_addr = Column(Base58DataType(CBitcoinAddress)) 102 | their_balance = Column(Integer) 103 | their_addr = Column(Base58DataType(CBitcoinAddress)) 104 | 105 | def signature(self, transaction): 106 | """Signature for a transaction.""" 107 | sighash = SignatureHash(CScript(self.anchor_redeem), 108 | transaction, 0, SIGHASH_ALL) 109 | sig = g.seckey.sign(sighash) + bytes([SIGHASH_ALL]) 110 | return sig 111 | 112 | def sign(self, transaction): 113 | """Sign a transaction.""" 114 | sig = self.signature(transaction) 115 | anchor_sig = AnchorScriptSig(self.anchor_index, 116 | self.their_sig, 117 | self.anchor_redeem) 118 | transaction.vin[0].scriptSig = anchor_sig.to_script(sig) 119 | # verify signing worked 120 | VerifyScript(transaction.vin[0].scriptSig, 121 | CScript(self.anchor_redeem).to_p2sh_scriptPubKey(), 122 | transaction, 0, (SCRIPT_VERIFY_P2SH,)) 123 | return transaction 124 | 125 | def commitment(self, ours=False): 126 | """Return an unsigned commitment transaction.""" 127 | first = CMutableTxOut(self.our_balance, self.our_addr.to_scriptPubKey()) 128 | second = CMutableTxOut(self.their_balance, self.their_addr.to_scriptPubKey()) 129 | if not ours: 130 | first, second = second, first 131 | return CMutableTransaction([CMutableTxIn(self.anchor_point)], 132 | [first, second]) 133 | 134 | def settlement(self): 135 | """Generate the settlement transaction.""" 136 | # Put outputs in the order of the inputs, so that both versions are the same 137 | first = CMutableTxOut(self.our_balance, 138 | self.our_addr.to_scriptPubKey()) 139 | second = CMutableTxOut(self.their_balance, 140 | self.their_addr.to_scriptPubKey()) 141 | if self.anchor_index == 0: 142 | pass 143 | elif self.anchor_index == 1: 144 | first, second = second, first 145 | else: 146 | raise Exception("Unknown index", self.anchor_index) 147 | return CMutableTransaction([CMutableTxIn(self.anchor_point)], 148 | [first, second]) 149 | 150 | def select_coins(amount): 151 | """Get a txin set and change to spend amount.""" 152 | coins = g.bit.listunspent() 153 | out = [] 154 | for coin in coins: 155 | if not coin['spendable']: 156 | continue 157 | out.append(CMutableTxIn(coin['outpoint'])) 158 | amount -= coin['amount'] 159 | if amount <= 0: 160 | break 161 | if amount > 0: 162 | raise Exception("Not enough money") 163 | change = CMutableTxOut( 164 | -amount, g.bit.getrawchangeaddress().to_scriptPubKey()) 165 | return out, change 166 | 167 | def anchor_script(my_pubkey, their_pubkey): 168 | """Generate the output script for the anchor transaction.""" 169 | script = CScript([2, my_pubkey, their_pubkey, 2, OP_CHECKMULTISIG]) 170 | return script 171 | 172 | def get_pubkey(): 173 | """Get a new pubkey.""" 174 | return g.seckey.pub 175 | 176 | def update_db(address, amount, sig): 177 | """Update the db for a payment.""" 178 | channel = Channel.query.get(address) 179 | channel.our_balance += amount 180 | channel.their_balance -= amount 181 | channel.their_sig = sig 182 | database.session.commit() 183 | return channel.signature(channel.commitment()) 184 | 185 | def create(url, mymoney, theirmoney, fees=10000): 186 | """Open a payment channel. 187 | 188 | After this method returns, a payment channel will have been established 189 | with the node identified by url, in which you can send mymoney satoshis 190 | and recieve theirmoney satoshis. Any blockchain fees involved in the 191 | setup and teardown of the channel should be collected at this time. 192 | """ 193 | bob = jsonrpcproxy.Proxy(url+'channel/') 194 | # Choose inputs and change output 195 | coins, change = select_coins(mymoney + 2 * fees) 196 | pubkey = get_pubkey() 197 | my_out_addr = g.bit.getnewaddress() 198 | # Tell Bob we want to open a channel 199 | transaction, redeem, their_out_addr = bob.open_channel( 200 | g.addr, theirmoney, mymoney, fees, 201 | coins, change, 202 | pubkey, my_out_addr) 203 | # Sign and send the anchor 204 | transaction = g.bit.signrawtransaction(transaction) 205 | assert transaction['complete'] 206 | transaction = transaction['tx'] 207 | g.bit.sendrawtransaction(transaction) 208 | # Set up the channel in the DB 209 | channel = Channel(address=url, 210 | anchor_point=COutPoint(transaction.GetHash(), 0), 211 | anchor_index=1, 212 | their_sig=b'', 213 | anchor_redeem=redeem, 214 | our_balance=mymoney, 215 | our_addr=my_out_addr, 216 | their_balance=theirmoney, 217 | their_addr=their_out_addr, 218 | ) 219 | # Exchange signatures for the inital commitment transaction 220 | channel.their_sig = \ 221 | bob.update_anchor(g.addr, transaction.GetHash(), 222 | channel.signature(channel.commitment())) 223 | database.session.add(channel) 224 | database.session.commit() 225 | # Event: channel opened 226 | CHANNEL_OPENED.send('channel', address=url) 227 | 228 | def send(url, amount): 229 | """Send coin in the channel. 230 | 231 | Negotiate the update of the channel opened with node url paying that node 232 | amount more satoshis than before. No fees should be collected by this 233 | method. 234 | """ 235 | bob = jsonrpcproxy.Proxy(url+'channel/') 236 | # ask Bob to sign the new commitment transactions, and update. 237 | sig = update_db(url, -amount, bob.propose_update(g.addr, amount)) 238 | # tell Bob our signature 239 | bob.recieve(g.addr, amount, sig) 240 | 241 | def getbalance(url): 242 | """Get the balance of funds in a payment channel. 243 | 244 | This returns the number of satoshis you can spend in the channel 245 | with the node at url. This should have no side effects. 246 | """ 247 | return Channel.query.get(url).our_balance 248 | 249 | def getcommitmenttransactions(url): 250 | """Get the current commitment transactions in a payment channel.""" 251 | channel = Channel.query.get(url) 252 | commitment = channel.sign(channel.commitment(ours=True)) 253 | return [commitment,] 254 | 255 | def close(url): 256 | """Close a channel. 257 | 258 | Close the currently open channel with node url. Any funds in the channel 259 | are paid to the wallet, along with any fees collected by create which 260 | were unnecessary.""" 261 | bob = jsonrpcproxy.Proxy(url+'channel/') 262 | channel = Channel.query.get(url) 263 | # Tell Bob we are closing the channel, and sign the settlement tx 264 | bob.close_channel(g.addr, channel.signature(channel.settlement())) 265 | database.session.delete(channel) 266 | database.session.commit() 267 | 268 | @REMOTE 269 | def info(): 270 | """Get bitcoind info.""" 271 | return g.bit.getinfo() 272 | 273 | @REMOTE 274 | def get_address(): 275 | """Get payment address.""" 276 | return str(g.bit.getnewaddress()) 277 | 278 | @REMOTE 279 | def open_channel(address, mymoney, theirmoney, fees, their_coins, their_change, their_pubkey, their_out_addr): # pylint: disable=too-many-arguments, line-too-long 280 | """Open a payment channel.""" 281 | # Get inputs and change output 282 | coins, change = select_coins(mymoney + 2 * fees) 283 | # Make the anchor script 284 | anchor_output_script = anchor_script(get_pubkey(), their_pubkey) 285 | # Construct the anchor utxo 286 | payment = CMutableTxOut(mymoney + theirmoney + 2 * fees, 287 | anchor_output_script.to_p2sh_scriptPubKey()) 288 | # Anchor tx 289 | transaction = CMutableTransaction( 290 | their_coins + coins, 291 | [payment, change, their_change]) 292 | # Half-sign 293 | transaction = g.bit.signrawtransaction(transaction)['tx'] 294 | # Create channel in DB 295 | our_addr = g.bit.getnewaddress() 296 | channel = Channel(address=address, 297 | anchor_point=COutPoint(transaction.GetHash(), 0), 298 | anchor_index=0, 299 | their_sig=b'', 300 | anchor_redeem=anchor_output_script, 301 | our_balance=mymoney, 302 | our_addr=our_addr, 303 | their_balance=theirmoney, 304 | their_addr=their_out_addr, 305 | ) 306 | database.session.add(channel) 307 | database.session.commit() 308 | # Event: channel opened 309 | CHANNEL_OPENED.send('channel', address=address) 310 | return (transaction, anchor_output_script, our_addr) 311 | 312 | @REMOTE 313 | def update_anchor(address, new_anchor, their_sig): 314 | """Update the anchor txid after both have signed.""" 315 | channel = Channel.query.get(address) 316 | channel.anchor_point = COutPoint(new_anchor, channel.anchor_point.n) 317 | channel.their_sig = their_sig 318 | database.session.commit() 319 | return channel.signature(channel.commitment()) 320 | 321 | @REMOTE 322 | def propose_update(address, amount): 323 | """Sign commitment transactions.""" 324 | channel = Channel.query.get(address) 325 | assert amount > 0 326 | channel.our_balance += amount 327 | channel.their_balance -= amount 328 | # don't persist yet 329 | sig = channel.signature(channel.commitment()) 330 | channel.our_balance -= amount 331 | channel.their_balance += amount 332 | return sig 333 | 334 | @REMOTE 335 | def recieve(address, amount, sig): 336 | """Recieve money.""" 337 | update_db(address, amount, sig) 338 | 339 | @REMOTE 340 | def close_channel(address, their_sig): 341 | """Close a channel.""" 342 | channel = Channel.query.get(address) 343 | # Sign and send settlement tx 344 | my_sig = channel.signature(channel.settlement()) 345 | channel.their_sig = their_sig 346 | transaction = channel.sign(channel.settlement()) 347 | g.bit.sendrawtransaction(transaction) 348 | database.session.delete(channel) 349 | database.session.commit() 350 | return my_sig 351 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """Parse configuration options for bitcoin and lightning. 4 | 5 | bitcoin_config returns the parsed contents of the bitcoin configuration file, 6 | including default values (currently not specified). 7 | lightning_config does the same for a lightning configuration file. 8 | 9 | bitcoin_proxy and lightning_proxy return RPC proxies to bitcoind and 10 | lightningd respectively. 11 | 12 | collect_proxies returns a ProxySet with proxies to bitcoin and lightning nodes, 13 | a url to the lightning node, and the pid_file of the lightning node. 14 | """ 15 | 16 | import os.path 17 | from configparser import ConfigParser 18 | import bitcoin.rpc 19 | import jsonrpcproxy 20 | from collections import namedtuple 21 | 22 | DEFAULT_DATADIR = os.path.expanduser("~/.bitcoin") 23 | 24 | def get_config(args=None, path=None, defaults=None): 25 | """Parse configs from file 'path', returning a dict-like object""" 26 | config = ConfigParser() 27 | if defaults is not None: 28 | config.read_dict({'config': defaults}) 29 | 30 | if path is not None and os.path.isfile(path): 31 | with open(path) as config_file: 32 | conf_data = config_file.read() 33 | config.read_string('[config]\n' + conf_data) 34 | assert config.sections() == ['config'] 35 | else: 36 | print("No configuration file found") 37 | 38 | if args is not None: 39 | config.read_dict({'config': args}) 40 | 41 | return config['config'] 42 | 43 | BITCOIN_DEFAULTS = { 44 | } 45 | def bitcoin_config(args=None, datadir=DEFAULT_DATADIR, conf="bitcoin.conf"): 46 | """Parse and return bitcoin config.""" 47 | conf_path = os.path.join(datadir, conf) 48 | return get_config(args=args, path=conf_path, defaults=BITCOIN_DEFAULTS) 49 | 50 | def bitcoin_proxy(args=None, datadir=DEFAULT_DATADIR, conf="bitcoin.conf"): 51 | """Return a bitcoin proxy pointing to the config.""" 52 | bitcoin_conf = bitcoin_config(args, datadir, conf) 53 | return bitcoin.rpc.Proxy('http://%s:%s@localhost:%d' % 54 | (bitcoin_conf.get('rpcuser'), 55 | bitcoin_conf.get('rpcpassword'), 56 | bitcoin_conf.getint('rpcport'))) 57 | 58 | LIGHTNING_DEFAULTS = { 59 | 'daemon':False, 60 | 'port':9333, 61 | 'pidfile':'lightning.pid', 62 | } 63 | def lightning_config(args=None, 64 | datadir=DEFAULT_DATADIR, 65 | conf="lightning.conf"): 66 | """Parse and return lightning config.""" 67 | conf_path = os.path.join(datadir, conf) 68 | return get_config(args=args, path=conf_path, defaults=LIGHTNING_DEFAULTS) 69 | 70 | def lightning_proxy(args=None, datadir=DEFAULT_DATADIR, conf="lightning.conf"): 71 | """Return a lightning proxy pointing to the config.""" 72 | lightning_conf = lightning_config(args, datadir, conf) 73 | return jsonrpcproxy.AuthProxy( 74 | 'http://localhost:%d/local/' % lightning_conf.getint('port'), 75 | (lightning_conf.get('rpcuser'), 76 | lightning_conf.get('rpcpassword'))) 77 | 78 | ProxySet = namedtuple('ProxySet', ['bit', 'lit', 'lurl', 'lpid']) 79 | def collect_proxies(datadir=DEFAULT_DATADIR): 80 | """Collect proxies for a given node. 81 | 82 | bit is a proxy to bitcoind 83 | lit is a proxy to lightningd 84 | lurl is a url for the lightning node 85 | lpid is the pid_file of lightningd 86 | """ 87 | bit = bitcoin_proxy(datadir=datadir) 88 | lit = lightning_proxy(datadir=datadir) 89 | lightning_conf = lightning_config(datadir=datadir) 90 | lurl = 'http://localhost:%d/' % lightning_conf.getint('port') 91 | lpid = os.path.join(datadir, 'lightning.pid') 92 | return ProxySet(bit, lit, lurl, lpid) 93 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | """Set up an environment for manipulating a lightning network. 2 | 3 | balances() returns a tuple of Alice, Bob, and Carol's total balances 4 | Carol has so much money because she is the miner. 5 | 6 | Send 0.1 BTC from Alice to Bob on the lightning network 7 | >>> alice.lit.send(bob.lurl, 10000000) 8 | True 9 | 10 | Check balances: 11 | >>> balances() 12 | (89970000, 109980000, 199970000) 13 | 14 | Read test.py for more example usage. 15 | """ 16 | 17 | import test.regnet as regnet 18 | NET = regnet.create(datadir=None) 19 | NET.generate(101) 20 | NET.miner.bit.sendmany( 21 | "", 22 | {NET[0].bit.getnewaddress(): 100000000, 23 | NET[1].bit.getnewaddress(): 100000000, 24 | NET[2].bit.getnewaddress(): 200000000, 25 | }) 26 | NET.sync() 27 | alice, bob, carol = NET[0], NET[1], NET[2] 28 | alice.lit.create(carol.lurl, 50000000, 50000000) 29 | NET.generate() 30 | bob.lit.create(carol.lurl, 50000000, 50000000) 31 | NET.generate() 32 | 33 | def balances(): 34 | """Return the total balances of Alice, Bob, and Carol.""" 35 | return (alice.bit.getbalance() + alice.lit.getbalance(carol.lurl), 36 | bob.bit.getbalance() + bob.lit.getbalance(carol.lurl), 37 | carol.bit.getbalance() + carol.lit.getbalance(alice.lurl) + \ 38 | carol.lit.getbalance(bob.lurl), 39 | ) 40 | -------------------------------------------------------------------------------- /jsonrpcproxy.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """JSON-RPC tools. 4 | 5 | Proxy is a simple RPC client with automatic method generation. It supports 6 | transparent translation of objects specified below. 7 | AuthProxy is the same as proxy but can authenticate itself with basic auth. 8 | 9 | SmartDispatcher is a server component which handles transparent translation 10 | of the objects specified below. 11 | 12 | Objects supported by transparent (automatic) translation: 13 | * int (standard JSON) 14 | * str (standard JSON) 15 | * None (usually caught by JSON-RPC library for notifications) 16 | * bytes 17 | * bitcoin.base58.CBase58Data 18 | - bitcoin.wallet.CBitcoinAddress 19 | * bitcoin.core.Serializable 20 | - bitcoin.core.CMutableTransaction 21 | - bitcoin.core.CMutableTxIn 22 | - bitcoin.core.CMutableTxOut 23 | * list, tuple (converted to list) (recursive) 24 | * dict (keys int or str) (not containing the key '__class__') (recursive) 25 | """ 26 | 27 | from functools import wraps 28 | import json 29 | from base64 import b64encode, b64decode 30 | import requests 31 | from jsonrpc.dispatcher import Dispatcher 32 | import bitcoin.core 33 | from bitcoin.core.serialize import Serializable 34 | import bitcoin.wallet 35 | import bitcoin.base58 36 | 37 | class ConversionError(Exception): 38 | """Error in conversion to or from JSON.""" 39 | 40 | def serialize_bytes(bytedata): 41 | """Convert bytes to str.""" 42 | return b64encode(bytedata).decode() 43 | 44 | def deserialize_bytes(b64data): 45 | """Convert str to bytes.""" 46 | return b64decode(b64data.encode()) 47 | 48 | def subclass_hook(encode, decode, allowed): 49 | """Generate encode/decode functions for one interface.""" 50 | lookup = {cls.__name__:cls for cls in allowed} 51 | def encode_subclass(message): 52 | """Convert message to JSON.""" 53 | for cls in allowed: 54 | if isinstance(message, cls): 55 | return {'subclass':cls.__name__, 56 | 'value':encode(message)} 57 | raise ConversionError("Unknown message type", type(message).__name__) 58 | def decode_subclass(message): 59 | """Recover message from JSON.""" 60 | cls = lookup[message['subclass']] 61 | return decode(cls, message['value']) 62 | return encode_subclass, decode_subclass 63 | 64 | HOOKS = [ 65 | (bitcoin.base58.CBase58Data, subclass_hook( 66 | str, 67 | lambda cls, message: cls(message), 68 | [ 69 | bitcoin.wallet.CBitcoinAddress, 70 | bitcoin.base58.CBase58Data, 71 | ])), 72 | (bytes, subclass_hook( 73 | serialize_bytes, 74 | lambda cls, message: cls(deserialize_bytes(message)), 75 | [ 76 | bytes, 77 | ])), 78 | (Serializable, subclass_hook( 79 | lambda message: serialize_bytes(message.serialize()), 80 | lambda cls, message: cls.deserialize(deserialize_bytes(message)), 81 | [ 82 | bitcoin.core.CMutableTransaction, 83 | bitcoin.core.CTransaction, 84 | bitcoin.core.CMutableTxIn, 85 | bitcoin.core.CMutableTxOut, 86 | ])), 87 | ] 88 | 89 | def to_json(message): 90 | """Prepare message for JSON serialization.""" 91 | if isinstance(message, list) or isinstance(message, tuple): 92 | return [to_json(sub) for sub in message] 93 | elif isinstance(message, dict): 94 | assert '__class__' not in message 95 | return {to_json(key):to_json(message[key]) for key in message} 96 | elif isinstance(message, int) or isinstance(message, str): 97 | return message 98 | elif message is None: 99 | return {'__class__':'None'} 100 | else: 101 | for cls, codes in HOOKS: 102 | if isinstance(message, cls): 103 | out = codes[0](message) 104 | assert '__class__' not in out 105 | out['__class__'] = cls.__name__ 106 | return out 107 | raise ConversionError("Unable to convert", message) 108 | 109 | def from_json(message): 110 | """Retrieve an object from JSON message (undo to_json).""" 111 | if isinstance(message, list) or isinstance(message, tuple): 112 | return [from_json(sub) for sub in message] 113 | elif isinstance(message, int) or isinstance(message, str): 114 | return message 115 | elif isinstance(message, dict): 116 | if '__class__' not in message: 117 | return {from_json(key):from_json(message[key]) for key in message} 118 | elif message['__class__'] == 'None': 119 | return None 120 | else: 121 | for cls, codes in HOOKS: 122 | if message['__class__'] == cls.__name__: 123 | return codes[1](message) 124 | raise ConversionError("Unable to convert", message) 125 | 126 | def convert_exception(exception): 127 | """Convert an exception's arguments to jsonizable form.""" 128 | def force_convert(value): 129 | """Try to convert reversibly, otherwise cast to string.""" 130 | try: 131 | return to_json(value) 132 | except ConversionError: 133 | return to_json(repr(value)) 134 | exception.args = [force_convert(arg) for arg in exception.args] 135 | return exception 136 | 137 | class SmartDispatcher(Dispatcher): 138 | """Wrap methods to allow complex objects in JSON RPC calls.""" 139 | 140 | def __getitem__(self, key): 141 | """Override __getitem__ to support transparent translation. 142 | 143 | Translate function arguments, return value, and exceptions. 144 | """ 145 | old_value = Dispatcher.__getitem__(self, key) 146 | @wraps(old_value) 147 | def wrapped(*args, **kwargs): 148 | """Wrap a function in JSON formatting.""" 149 | try: 150 | return to_json(old_value(*from_json(args), 151 | **from_json(kwargs))) 152 | except Exception as exception: 153 | convert_exception(exception) 154 | raise 155 | return wrapped 156 | 157 | class JSONResponseException(Exception): 158 | """Exception returned from RPC call""" 159 | 160 | class JSONRPCError(Exception): 161 | """Error making RPC call""" 162 | 163 | class Proxy(object): 164 | """Remote method call proxy.""" 165 | 166 | def __init__(self, url): 167 | self.url = url 168 | self.headers = {'content-type': 'application/json'} 169 | self._id = 0 170 | 171 | def _call(self, name, *args, **kwargs): 172 | """Call a method.""" 173 | args, kwargs = to_json(args), to_json(kwargs) 174 | assert not (args and kwargs) 175 | payload = { 176 | 'method': name, 177 | 'params': kwargs or args, 178 | 'id': self._id, 179 | 'jsonrpc': '2.0' 180 | } 181 | 182 | response = self._request(json.dumps(payload)) 183 | 184 | assert response["jsonrpc"] == "2.0" 185 | assert response["id"] == self._id 186 | 187 | self._id += 1 188 | 189 | if 'error' in response: 190 | raise JSONResponseException(from_json(response['error'])) 191 | elif 'result' not in response: 192 | raise JSONRPCError('missing JSON RPC result') 193 | else: 194 | return from_json(response['result']) 195 | 196 | def _request(self, data): 197 | """Perform the request.""" 198 | return requests.post(self.url, data=data, headers=self.headers).json() 199 | 200 | def __getattr__(self, name): 201 | """Generate method stubs as needed.""" 202 | func = lambda *args, **kwargs: self._call(name, *args, **kwargs) 203 | func.__name__ = name 204 | return func 205 | 206 | class AuthProxy(Proxy): 207 | """Proxy with basic authentication.""" 208 | def __init__(self, url, auth): 209 | Proxy.__init__(self, url) 210 | self.auth = auth 211 | 212 | def _request(self, data): 213 | """Perform the request.""" 214 | return requests.post( 215 | self.url, data=data, headers=self.headers, auth=self.auth).json() 216 | -------------------------------------------------------------------------------- /lightning.py: -------------------------------------------------------------------------------- 1 | """Lightning network API for a lightning node. 2 | 3 | Interface: 4 | API -- the Blueprint returned by serverutil.api_factory 5 | 6 | init(conf) - Set up the database 7 | send(url, amount) 8 | - Send amount satoshis to the node identified by url. The url is not 9 | necessarily a direct peer. 10 | 11 | Error conditions have not been defined. 12 | 13 | Database: 14 | PEERS contains information nodes we have channels open with 15 | address: their url 16 | fees: how much we require as fees for relaying across this channel 17 | 18 | ROUTES is the routing table. It has one row for every other lightning node 19 | address: their url 20 | cost: total fees to route payment to that node 21 | nexthop: where should payment go next on the path to that node 22 | """ 23 | 24 | from flask import g 25 | import jsonrpcproxy 26 | from serverutil import api_factory, database 27 | import channel 28 | from sqlalchemy import Column, Integer, String 29 | 30 | API, REMOTE, Model = api_factory('lightning') 31 | 32 | class Peer(Model): 33 | """Database model of a peer node.""" 34 | 35 | __tablename__ = "peers" 36 | 37 | address = Column(String, primary_key=True) 38 | fees = Column(Integer) 39 | 40 | class Route(Model): 41 | """Database model of a route.""" 42 | 43 | __tablename__ = "routes" 44 | 45 | address = Column(String, primary_key=True) 46 | cost = Column(Integer) 47 | next_hop = Column(String) 48 | 49 | @channel.CHANNEL_OPENED.connect_via('channel') 50 | def on_open(dummy_sender, address, **dummy_args): 51 | """Routing update on open.""" 52 | fees = 10000 53 | # Add the new peer 54 | peer = Peer(address=address, fees=fees) 55 | database.session.add(peer) 56 | # Broadcast a routing update 57 | update(address, address, 0) 58 | # The new peer doesn't know all our routes. 59 | # As a hack, rebuild/rebroadcast the whole routing table. 60 | routes = Route.query.all() 61 | Route.query.delete() 62 | database.session.commit() 63 | for route in routes: 64 | update(route.next_hop, route.address, route.cost) 65 | 66 | @REMOTE 67 | def update(next_hop, address, cost): 68 | """Routing update.""" 69 | # Check previous route, and only update if this is an improvement 70 | if address == g.addr: 71 | return 72 | route = Route.query.get(address) 73 | if route is None: 74 | route = Route(address=address, cost=cost, next_hop=next_hop) 75 | database.session.add(route) 76 | elif route.cost <= cost: 77 | return 78 | else: 79 | route.cost = cost 80 | route.next_hop = next_hop 81 | # Tell all our peers 82 | database.session.commit() 83 | for peer in Peer.query.all(): 84 | bob = jsonrpcproxy.Proxy(peer.address + 'lightning/') 85 | bob.update(g.addr, address, cost + peer.fees) 86 | return True 87 | 88 | @REMOTE 89 | def send(url, amount): 90 | """Send coin, perhaps through more than one hop. 91 | 92 | After this call, the node at url should have recieved amount satoshis. 93 | Any fees should be collected from this node's balance. 94 | """ 95 | # Paying ourself is easy 96 | if url == g.addr: 97 | return 98 | route = Route.query.get(url) 99 | if route is None: 100 | # If we don't know how to get there, let channel try. 101 | # As set up currently, this shouldn't ever work, but 102 | # we can let channel throw the error 103 | channel.send(url, amount) 104 | else: 105 | # Send the next hop money over our payment channel 106 | channel.send(route.next_hop, amount + route.cost) 107 | # Ask the next hop to send money to the destination 108 | bob = jsonrpcproxy.Proxy(route.next_hop+'lightning/') 109 | bob.send(url, amount) 110 | -------------------------------------------------------------------------------- /lightningd.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """Parse configuration and start the server. 4 | 5 | When run with -daemon, daemonize the process first. 6 | 7 | Usage: 8 | -daemon: daemonize (Default False) 9 | -debug: use the debug server (Currently conflicts with daemon) 10 | -datadir=: specify the directory to run in 11 | -conf=: specify the configuration file (default lightning.conf) 12 | -port=: specify the port to bind to 13 | 14 | Options except for datadir and conf can be specified in the configuration file. 15 | Command line options take precedence over configuration file options. 16 | Flag options can be turned off by prefixing with 'no' (Ex: -nodaemon). 17 | """ 18 | 19 | import argparse 20 | import config 21 | import os 22 | import os.path 23 | import json 24 | import hashlib 25 | from flask import request, current_app, g 26 | import bitcoin.rpc 27 | from bitcoin.wallet import CBitcoinSecret 28 | from serverutil import app 29 | from serverutil import requires_auth 30 | from serverutil import WALLET_NOTIFY, BLOCK_NOTIFY 31 | import channel 32 | import lightning 33 | import local 34 | 35 | @app.before_request 36 | def before_request(): 37 | """Setup g context""" 38 | g.config = current_app.config 39 | g.bit = g.config['bitcoind'] 40 | secret = hashlib.sha256(g.config['secret']).digest() 41 | g.seckey = CBitcoinSecret.from_secret_bytes(secret) 42 | g.addr = 'http://localhost:%d/' % int(g.config['port']) 43 | g.logger = current_app.logger 44 | 45 | @app.route('/error') 46 | @requires_auth 47 | def error(): 48 | """Raise an error.""" 49 | raise Exception("Hello") 50 | 51 | @app.route("/get-ip") 52 | def get_my_ip(): 53 | """Return remote_addr.""" 54 | return json.dumps({'ip': request.remote_addr}), 200 55 | 56 | @app.route('/info') 57 | @requires_auth 58 | def infoweb(): 59 | """Get bitcoind info.""" 60 | return str(app.config['bitcoind'].getinfo()) 61 | 62 | @app.route('/wallet-notify') 63 | @requires_auth 64 | def wallet_notify(): 65 | """Process a wallet notification.""" 66 | WALLET_NOTIFY.send('server', tx=request.args['tx']) 67 | return "Done" 68 | 69 | @app.route('/block-notify') 70 | @requires_auth 71 | def block_notify(): 72 | """Process a block notification.""" 73 | BLOCK_NOTIFY.send('server', block=request.args['block']) 74 | return "Done" 75 | 76 | if __name__ == '__main__': 77 | parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS) 78 | def add_switch(name): 79 | """Set up command line arguments to turn a switch on and off.""" 80 | group = parser.add_mutually_exclusive_group() 81 | group.add_argument('-'+name, dest=name, action='store_true') 82 | group.add_argument('-no'+name, dest=name, action='store_false') 83 | parser.add_argument('-datadir', default=config.DEFAULT_DATADIR) 84 | parser.add_argument('-conf', default='lightning.conf') 85 | add_switch('debug') 86 | parser.add_argument('-port') 87 | args = parser.parse_args() 88 | conf = config.lightning_config(args=vars(args), 89 | datadir=args.datadir, 90 | conf=args.conf) 91 | 92 | with open(os.path.join(conf['datadir'], conf['pidfile']), 'w') as pid_file: 93 | pid_file.write(str(os.getpid())) 94 | 95 | if conf.getboolean('regtest'): 96 | bitcoin.SelectParams('regtest') 97 | else: 98 | raise Exception("Non-regnet use not supported") 99 | 100 | port = conf.getint('port') 101 | app.config['secret'] = b'correct horse battery staple' + bytes(str(port), 'utf8') 102 | app.config.update(conf) 103 | app.config['bitcoind'] = bitcoin.rpc.Proxy('http://%s:%s@localhost:%d' % 104 | (conf['bituser'], conf['bitpass'], 105 | int(conf['bitport']))) 106 | app.config['SQLALCHEMY_BINDS'] = {} 107 | app.register_blueprint(channel.API) 108 | app.register_blueprint(lightning.API) 109 | app.register_blueprint(local.API) 110 | 111 | app.run(port=port, debug=conf.getboolean('debug'), use_reloader=False, 112 | processes=3) 113 | -------------------------------------------------------------------------------- /local.py: -------------------------------------------------------------------------------- 1 | """Local (private) API for a lightning node. 2 | 3 | Currently this just collects and exposes methods in channel and lightning. 4 | A HTML GUI could also be provided here in the future. 5 | All requests require authentication. 6 | """ 7 | 8 | from serverutil import api_factory, authenticate_before_request 9 | import channel, lightning 10 | 11 | API, REMOTE, Model = api_factory('local') 12 | 13 | REMOTE(channel.create) 14 | REMOTE(lightning.send) 15 | REMOTE(channel.close) 16 | REMOTE(channel.getbalance) 17 | REMOTE(channel.getcommitmenttransactions) 18 | 19 | @REMOTE 20 | def alive(): 21 | """Test if the server is ready to handle requests.""" 22 | return True 23 | 24 | API.before_request(authenticate_before_request) 25 | -------------------------------------------------------------------------------- /notify.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """A little curl-ish program.""" 4 | 5 | import sys 6 | import requests 7 | 8 | PORT = int(sys.argv[3]) 9 | URL = 'http://localhost:%d/' % PORT 10 | if sys.argv[1] == 'block': 11 | URL += 'block-notify?block=%s' % sys.argv[2] 12 | elif sys.argv[1] == 'wallet': 13 | URL += 'wallet-notify?tx=%s' % sys.argv[2] 14 | else: 15 | raise Exception("Unknown notification", sys.argv[1]) 16 | requests.get(URL, auth=('rt', 'rt')) 17 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # Use multiple processes to speed up Pylint. 25 | jobs=1 26 | 27 | # Allow loading of arbitrary C extensions. Extensions are imported into the 28 | # active Python interpreter and may run arbitrary code. 29 | unsafe-load-any-extension=no 30 | 31 | # A comma-separated list of package or module names from where C extensions may 32 | # be loaded. Extensions are loading into the active Python interpreter and may 33 | # run arbitrary code 34 | extension-pkg-whitelist= 35 | 36 | # Allow optimization of some AST trees. This will activate a peephole AST 37 | # optimizer, which will apply various small optimizations. For instance, it can 38 | # be used to obtain the result of joining multiple strings with the addition 39 | # operator. Joining a lot of strings can lead to a maximum recursion error in 40 | # Pylint and this flag can prevent that. It has one side effect, the resulting 41 | # AST will be different than the one from reality. 42 | optimize-ast=no 43 | 44 | 45 | [REPORTS] 46 | 47 | # Set the output format. Available formats are text, parseable, colorized, msvs 48 | # (visual studio) and html. You can also give a reporter class, eg 49 | # mypackage.mymodule.MyReporterClass. 50 | output-format=text 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=no 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | 76 | [MESSAGES CONTROL] 77 | 78 | # Only show warnings with the listed confidence levels. Leave empty to show 79 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 80 | confidence= 81 | 82 | # Enable the message, report, category or checker with the given id(s). You can 83 | # either give multiple identifier separated by comma (,) or put this option 84 | # multiple time. See also the "--disable" option for examples. 85 | #enable= 86 | 87 | # Disable the message, report, category or checker with the given id(s). You 88 | # can either give multiple identifiers separated by comma (,) or put this 89 | # option multiple times (only on the command line, not in the configuration 90 | # file where it should appear only once).You can also use "--disable=all" to 91 | # disable everything first and then reenable specific checks. For example, if 92 | # you want to run only the similarities checker, you can use "--disable=all 93 | # --enable=similarities". If you want to run only the classes checker, but have 94 | # no Warning level messages displayed, use"--disable=all --enable=classes 95 | # --disable=W" 96 | disable=W1603,W1610,W1635,W1638,W1606,W1613,W1637,W1620,W1608,W1618,W1602,W1633,E1608,I0021,W1639,W1628,E1606,I0020,W1612,W1611,W1605,W1626,W1619,W1624,W0704,W1625,W1623,W1630,E1604,W1632,E1602,W1621,W1640,E1603,W1609,W1617,E1607,W1614,W1607,W1627,W1636,W1622,W1604,W1629,W1615,W1601,W1634,W1616,E1605,E1601 97 | 98 | 99 | [BASIC] 100 | 101 | # Required attributes for module, separated by a comma 102 | required-attributes= 103 | 104 | # List of builtins function names that should not be used, separated by a comma 105 | bad-functions=map,filter 106 | 107 | # Good variable names which should always be accepted, separated by a comma 108 | good-names=i,j,k,ex,Run,_ 109 | 110 | # Bad variable names which should always be refused, separated by a comma 111 | bad-names=foo,bar,baz,toto,tutu,tata 112 | 113 | # Colon-delimited sets of names that determine each other's naming style when 114 | # the name regexes allow several styles. 115 | name-group= 116 | 117 | # Include a hint for the correct naming format with invalid-name 118 | include-naming-hint=no 119 | 120 | # Regular expression matching correct class attribute names 121 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 122 | 123 | # Naming hint for class attribute names 124 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 125 | 126 | # Regular expression matching correct module names 127 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 128 | 129 | # Naming hint for module names 130 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 131 | 132 | # Regular expression matching correct class names 133 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 134 | 135 | # Naming hint for class names 136 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 137 | 138 | # Regular expression matching correct variable names 139 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 140 | 141 | # Naming hint for variable names 142 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 143 | 144 | # Regular expression matching correct constant names 145 | const-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 146 | 147 | # Naming hint for constant names 148 | const-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 149 | 150 | # Regular expression matching correct argument names 151 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 152 | 153 | # Naming hint for argument names 154 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 155 | 156 | # Regular expression matching correct function names 157 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 158 | 159 | # Naming hint for function names 160 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 161 | 162 | # Regular expression matching correct attribute names 163 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 164 | 165 | # Naming hint for attribute names 166 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 167 | 168 | # Regular expression matching correct inline iteration names 169 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 170 | 171 | # Naming hint for inline iteration names 172 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 173 | 174 | # Regular expression matching correct method names 175 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 176 | 177 | # Naming hint for method names 178 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 179 | 180 | # Regular expression which should only match function or class names that do 181 | # not require a docstring. 182 | no-docstring-rgx=__.*__ 183 | 184 | # Minimum line length for functions/classes that require docstrings, shorter 185 | # ones are exempt. 186 | docstring-min-length=-1 187 | 188 | 189 | [TYPECHECK] 190 | 191 | # Tells whether missing members accessed in mixin class should be ignored. A 192 | # mixin class is detected if its name ends with "mixin" (case insensitive). 193 | ignore-mixin-members=yes 194 | 195 | # List of module names for which member attributes should not be checked 196 | # (useful for modules/projects where namespaces are manipulated during runtime 197 | # and thus existing member attributes cannot be deduced by static analysis 198 | ignored-modules= 199 | 200 | # List of classes names for which member attributes should not be checked 201 | # (useful for classes with attributes dynamically set). 202 | ignored-classes=SQLObject,scoped_session 203 | 204 | # When zope mode is activated, add a predefined set of Zope acquired attributes 205 | # to generated-members. 206 | zope=no 207 | 208 | # List of members which are set dynamically and missed by pylint inference 209 | # system, and so shouldn't trigger E0201 when accessed. Python regular 210 | # expressions are accepted. 211 | generated-members= 212 | 213 | 214 | [VARIABLES] 215 | 216 | # Tells whether we should check for unused import in __init__ files. 217 | init-import=no 218 | 219 | # A regular expression matching the name of dummy variables (i.e. expectedly 220 | # not used). 221 | dummy-variables-rgx=_$|dummy 222 | 223 | # List of additional names supposed to be defined in builtins. Remember that 224 | # you should avoid to define new builtins when possible. 225 | additional-builtins= 226 | 227 | # List of strings which can identify a callback function by name. A callback 228 | # name must start or end with one of those strings. 229 | callbacks=cb_,_cb 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [FORMAT] 254 | 255 | # Maximum number of characters on a single line. 256 | max-line-length=100 257 | 258 | # Regexp for a line that is allowed to be longer than the limit. 259 | ignore-long-lines=^\s*(# )??$ 260 | 261 | # Allow the body of an if to be on the same line as the test if there is no 262 | # else. 263 | single-line-if-stmt=no 264 | 265 | # List of optional constructs for which whitespace checking is disabled 266 | no-space-check=trailing-comma,dict-separator 267 | 268 | # Maximum number of lines in a module 269 | max-module-lines=1000 270 | 271 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 272 | # tab). 273 | indent-string=' ' 274 | 275 | # Number of spaces of indent required inside a hanging or continued line. 276 | indent-after-paren=4 277 | 278 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 279 | expected-line-ending-format= 280 | 281 | 282 | [SPELLING] 283 | 284 | # Spelling dictionary name. Available dictionaries: none. To make it working 285 | # install python-enchant package. 286 | spelling-dict= 287 | 288 | # List of comma separated words that should not be checked. 289 | spelling-ignore-words= 290 | 291 | # A path to a file that contains private dictionary; one word per line. 292 | spelling-private-dict-file= 293 | 294 | # Tells whether to store unknown words to indicated private dictionary in 295 | # --spelling-private-dict-file option instead of raising a message. 296 | spelling-store-unknown-words=no 297 | 298 | 299 | [LOGGING] 300 | 301 | # Logging modules to check that the string format arguments are in logging 302 | # function parameter format 303 | logging-modules=logging 304 | 305 | 306 | [IMPORTS] 307 | 308 | # Deprecated modules which should not be used, separated by a comma 309 | deprecated-modules=stringprep,optparse 310 | 311 | # Create a graph of every (i.e. internal and external) dependencies in the 312 | # given file (report RP0402 must not be disabled) 313 | import-graph= 314 | 315 | # Create a graph of external dependencies in the given file (report RP0402 must 316 | # not be disabled) 317 | ext-import-graph= 318 | 319 | # Create a graph of internal dependencies in the given file (report RP0402 must 320 | # not be disabled) 321 | int-import-graph= 322 | 323 | 324 | [CLASSES] 325 | 326 | # List of interface methods to ignore, separated by a comma. This is used for 327 | # instance to not check methods defines in Zope's Interface base class. 328 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 329 | 330 | # List of method names used to declare (i.e. assign) instance attributes. 331 | defining-attr-methods=__init__,__new__,setUp 332 | 333 | # List of valid names for the first argument in a class method. 334 | valid-classmethod-first-arg=cls 335 | 336 | # List of valid names for the first argument in a metaclass class method. 337 | valid-metaclass-classmethod-first-arg=mcs 338 | 339 | # List of member names, which should be excluded from the protected access 340 | # warning. 341 | exclude-protected=_asdict,_fields,_replace,_source,_make 342 | 343 | 344 | [DESIGN] 345 | 346 | # Maximum number of arguments for function / method 347 | max-args=5 348 | 349 | # Argument names that match this expression will be ignored. Default to name 350 | # with leading underscore 351 | ignored-argument-names=_.* 352 | 353 | # Maximum number of locals for function / method body 354 | max-locals=15 355 | 356 | # Maximum number of return / yield for function / method body 357 | max-returns=6 358 | 359 | # Maximum number of branch for function / method body 360 | max-branches=12 361 | 362 | # Maximum number of statements in function / method body 363 | max-statements=50 364 | 365 | # Maximum number of parents for a class (see R0901). 366 | max-parents=7 367 | 368 | # Maximum number of attributes for a class (see R0902). 369 | max-attributes=7 370 | 371 | # Minimum number of public methods for a class (see R0903). 372 | min-public-methods=0 373 | 374 | # Maximum number of public methods for a class (see R0904). 375 | max-public-methods=20 376 | 377 | 378 | [EXCEPTIONS] 379 | 380 | # Exceptions that will emit a warning when being caught. Defaults to 381 | # "Exception" 382 | overgeneral-exceptions=Exception 383 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-SQLAlchemy==2.0 3 | Jinja2==2.7.3 4 | MarkupSafe==0.23 5 | SQLAlchemy==1.0.8 6 | Werkzeug==0.10.4 7 | astroid==1.3.6 8 | blinker==1.4 9 | docutils==0.12 10 | itsdangerous==0.24 11 | json-rpc==1.10.2 12 | lockfile==0.10.2 13 | logilab-common==1.0.2 14 | protobuf==3.0.0a3 15 | pylint==1.4.4 16 | python-bitcoinlib==0.4.0 17 | python-daemon==2.0.5 18 | requests==2.7.0 19 | simplejson==3.8.0 20 | six==1.9.0 21 | -------------------------------------------------------------------------------- /serverutil.py: -------------------------------------------------------------------------------- 1 | """Utility functions for the server. 2 | 3 | This includes the interface from the server implementation to the 4 | payment channel and lightning network APIs. 5 | 6 | requires_auth -- decorator which makes a view function require authentication 7 | authenticate_before_request -- a before_request callback for auth 8 | api_factory -- returns a flask Blueprint or equivalent, along with a decorator 9 | making functions availiable as RPCs, and a base class for 10 | SQLAlchemy Declarative database models. 11 | 12 | Signals: 13 | WALLET_NOTIFY: sent when bitcoind tells us it has a transaction. 14 | - tx = txid 15 | BLOCK_NOTIFY: send when bitcoind tells us it has a block 16 | - block = block hash 17 | """ 18 | 19 | import os.path 20 | from functools import wraps 21 | from flask import Flask, current_app, Response, request, Blueprint 22 | from flask_sqlalchemy import SQLAlchemy 23 | from sqlalchemy.types import TypeDecorator 24 | from blinker import Namespace 25 | from sqlalchemy import LargeBinary, Text 26 | from jsonrpc.backend.flask import JSONRPCAPI 27 | import bitcoin.core.serialize 28 | from jsonrpcproxy import SmartDispatcher 29 | 30 | app = Flask(__name__) 31 | database = SQLAlchemy(app) 32 | 33 | SIGNALS = Namespace() 34 | WALLET_NOTIFY = SIGNALS.signal('WALLET_NOTIFY') 35 | BLOCK_NOTIFY = SIGNALS.signal('BLOCK_NOTIFY') 36 | 37 | # Copied from http://flask.pocoo.org/snippets/8/ 38 | def check_auth(username, password): 39 | """This function is called to check if a username / 40 | password combination is valid. 41 | """ 42 | return (username == current_app.config['rpcuser'] and 43 | password == current_app.config['rpcpassword']) 44 | 45 | def authenticate(): 46 | """Sends a 401 response that enables basic auth""" 47 | return Response( 48 | 'Could not verify your access level for that URL.\n' 49 | 'You have to login with proper credentials', 401, 50 | {'WWW-Authenticate': 'Basic realm="Login Required"'}) 51 | 52 | def requires_auth(view): 53 | """Require basic authentication on requests to this view. 54 | 55 | Also only accept requests from localhost. 56 | """ 57 | @wraps(view) 58 | def decorated(*args, **kwargs): 59 | """Decorated version of view that checks authentication.""" 60 | auth = request.authorization 61 | if not auth or not check_auth(auth.username, auth.password): 62 | return authenticate() 63 | if request.remote_addr != "127.0.0.1": 64 | return Response("Access outside 127.0.0.1 forbidden", 403) 65 | return view(*args, **kwargs) 66 | return decorated 67 | 68 | def authenticate_before_request(): 69 | """before_request callback to perform authentication.""" 70 | return requires_auth(lambda: None)() 71 | 72 | def api_factory(name): 73 | """Construct a Blueprint and a REMOTE decorator to set up an API. 74 | 75 | RPC calls are availiable at the url /name/ 76 | """ 77 | api = Blueprint(name, __name__, url_prefix='/'+name) 78 | 79 | # set up the database 80 | def setup_bind(state): 81 | """Add the database to the config.""" 82 | database_path = os.path.join(state.app.config['datadir'], name + '.dat') 83 | state.app.config['SQLALCHEMY_BINDS'][name] = 'sqlite:///' + database_path 84 | api.record_once(setup_bind) 85 | def initialize_database(): 86 | """Create the database.""" 87 | database.create_all(name) 88 | api.before_app_first_request(initialize_database) 89 | 90 | # create a base class for models 91 | class BoundMeta(type(database.Model)): 92 | """Metaclass for Model which allows __abstract__ base classes.""" 93 | def __init__(self, cls_name, bases, attrs): 94 | assert '__bind_key__' not in attrs 95 | if not attrs.get('__abstract__', False): 96 | attrs['__bind_key__'] = name 97 | super(BoundMeta, self).__init__(cls_name, bases, attrs) 98 | class BoundModel(database.Model, metaclass=BoundMeta): 99 | """Base class for models which have __bind_key__ set automatically.""" 100 | __abstract__ = True 101 | query = object.__getattribute__(database.Model, 'query') 102 | def __init__(self, *args, **kwargs): 103 | super(BoundModel, self).__init__(*args, **kwargs) 104 | 105 | # create a JSON-RPC API endpoint 106 | rpc_api = JSONRPCAPI(SmartDispatcher()) 107 | assert type(rpc_api.dispatcher == SmartDispatcher) 108 | api.add_url_rule('/', 'rpc', rpc_api.as_view(), methods=['POST']) 109 | 110 | return api, rpc_api.dispatcher.add_method, BoundModel 111 | 112 | class ImmutableSerializableType(TypeDecorator): 113 | """Converts bitcoin-lib ImmutableSerializable instances for the DB.""" 114 | 115 | impl = LargeBinary 116 | 117 | def __init__(self, subtype=bitcoin.core.serialize.ImmutableSerializable): 118 | self.subtype = subtype 119 | super(ImmutableSerializableType, self).__init__() 120 | 121 | @property 122 | def python_type(self): 123 | return self.subtype 124 | 125 | def process_bind_param(self, value, dialect): 126 | if value is not None: 127 | value = value.serialize() 128 | return value 129 | 130 | def process_result_value(self, value, dialect): 131 | if value is not None: 132 | value = self.subtype.deserialize(value) 133 | return value 134 | 135 | def process_literal_param(self, value, dialect): 136 | raise NotImplementedError() 137 | 138 | 139 | class Base58DataType(TypeDecorator): 140 | """Converts bitcoin-lib Base58Data instances for the DB.""" 141 | 142 | impl = Text 143 | 144 | def __init__(self, subtype=bitcoin.base58.CBase58Data): 145 | self.subtype = subtype 146 | super(Base58DataType, self).__init__() 147 | 148 | @property 149 | def python_type(self): 150 | return self.subtype 151 | 152 | def process_bind_param(self, value, dummy_dialect): 153 | if value is not None: 154 | value = str(value) 155 | return value 156 | 157 | def process_result_value(self, value, dummy_dialect): 158 | if value is not None: 159 | value = self.subtype(value) 160 | return value 161 | 162 | def process_literal_param(self, value, dialect): 163 | raise NotImplementedError() 164 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashplex/Lightning/e60986c6f4c9973fff0cc73f67b4e159e26e9ca9/test/__init__.py -------------------------------------------------------------------------------- /test/regnet.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """Manipulate regtest environments.""" 4 | 5 | import os 6 | import os.path 7 | import http.client 8 | import shutil 9 | import subprocess 10 | import signal 11 | import itertools 12 | from contextlib import contextmanager 13 | import time 14 | import tempfile 15 | import requests.exceptions 16 | import bitcoin 17 | import bitcoin.rpc 18 | import jsonrpcproxy 19 | bitcoin.SelectParams('regtest') 20 | 21 | BITCOIND = os.path.abspath('bitcoind') 22 | assert os.path.isfile(BITCOIND) 23 | LIGHTNINGD = os.path.abspath('lightningd.py') 24 | assert os.path.isfile(LIGHTNINGD) 25 | NOTIFY = os.path.abspath('notify.py') 26 | assert os.path.isfile(NOTIFY) 27 | 28 | PORT = itertools.count(18000) 29 | 30 | def get_port(): 31 | """Return a (hopefully open) port.""" 32 | return next(PORT) 33 | 34 | class BitcoinNode(object): 35 | """Interface to a bitcoind instance.""" 36 | 37 | CACHE_FILES = ['wallet.dat'] 38 | CACHE_DIRS = ['blocks', 'chainstate'] 39 | 40 | def __init__(self, datadir=None, cache=None, peers=None): 41 | if peers is None: 42 | peers = [] 43 | 44 | if datadir is None: 45 | self.datadir = tempfile.mkdtemp() 46 | else: 47 | os.mkdir(datadir, 0o700) 48 | self.datadir = datadir 49 | 50 | self.p2p_port = get_port() 51 | self.rpc_port = get_port() 52 | 53 | notify_path = os.path.join(self.datadir, 'notify.sh') 54 | with open(notify_path, 'w'): 55 | pass 56 | 57 | with open(os.path.join(self.datadir, 'bitcoin.conf'), 'w') as conf: 58 | conf.write("regtest=1\n") 59 | conf.write("walletnotify=sh %s wallet %%s\n" % notify_path) 60 | conf.write("blocknotify=sh %s block %%s\n" % notify_path) 61 | conf.write("rpcuser=rt\n") 62 | conf.write("rpcpassword=rt\n") 63 | conf.write("port=%d\n" % self.p2p_port) 64 | conf.write("rpcport=%d\n" % self.rpc_port) 65 | for peer in peers: 66 | conf.write("connect=localhost:%d\n" % peer.p2p_port) 67 | 68 | if cache is not None: 69 | restore_dir = os.path.join(self.datadir, 'regtest') 70 | os.mkdir(restore_dir) 71 | for cached_file in self.CACHE_FILES: 72 | shutil.copy(os.path.join(cache.name, cached_file), 73 | os.path.join(restore_dir, cached_file)) 74 | for cached_dir in self.CACHE_DIRS: 75 | shutil.copytree(os.path.join(cache.name, cached_dir), 76 | os.path.join(restore_dir, cached_dir)) 77 | 78 | self.process, self.proxy = None, None 79 | self.start() 80 | 81 | def start(self): 82 | """Start the node.""" 83 | self.process = subprocess.Popen( 84 | [ 85 | BITCOIND, '-datadir=%s' % self.datadir, '-debug', 86 | '-regtest', '-txindex', '-listen', '-relaypriority=0', 87 | '-discover=0', 88 | ], 89 | stdin=subprocess.DEVNULL, 90 | stdout=subprocess.DEVNULL, 91 | stderr=subprocess.STDOUT) 92 | self.proxy = bitcoin.rpc.Proxy('http://rt:rt@localhost:%d' % self.rpc_port) 93 | 94 | def stop(self, hard=False, cleanup=False): 95 | """Stop bitcoind.""" 96 | if hard: 97 | try: 98 | self.process.kill() 99 | except ProcessLookupError: 100 | pass 101 | else: 102 | self.proxy.stop() 103 | self.process.wait() 104 | if cleanup: 105 | self.cleanup() 106 | 107 | def cleanup(self): 108 | """Remove the files.""" 109 | shutil.rmtree(self.datadir, ignore_errors=True) 110 | 111 | @contextmanager 112 | def paused(self): 113 | """Context manager to pause a node.""" 114 | self.stop(hard=False, cleanup=False) 115 | yield 116 | self.start() 117 | self.wait_alive() 118 | 119 | def print_log(self): 120 | """Print the log file.""" 121 | with open(os.path.join(self.datadir, 'regtest', 'debug.log')) as log: 122 | print(log.read()) 123 | 124 | def cache(self): 125 | """Return an object which can be used to restart bitcoind. 126 | 127 | This should be called after stopping the node. 128 | """ 129 | cache_dir = tempfile.TemporaryDirectory() 130 | restore_dir = os.path.join(self.datadir, 'regtest') 131 | for cached_file in self.CACHE_FILES: 132 | shutil.copy(os.path.join(restore_dir, cached_file), 133 | os.path.join(cache_dir.name, cached_file)) 134 | for cached_dir in self.CACHE_DIRS: 135 | shutil.copytree(os.path.join(restore_dir, cached_dir), 136 | os.path.join(cache_dir.name, cached_dir)) 137 | return cache_dir 138 | 139 | def sync_state(self): 140 | """Compare for synchronization across the network.""" 141 | return set(self.proxy.getrawmempool()), self.proxy.getblockcount() 142 | 143 | def generate(self, blocks=1): 144 | """Generate blocks.""" 145 | self.proxy.generate(blocks) 146 | 147 | def is_alive(self): 148 | """Test if the node is alive.""" 149 | try: 150 | self.proxy.getinfo() 151 | except (ConnectionRefusedError, ConnectionResetError): 152 | pass 153 | except bitcoin.rpc.JSONRPCException as err: 154 | if err.error['code'] == -28: 155 | pass 156 | else: 157 | raise 158 | except http.client.BadStatusLine: 159 | pass 160 | else: 161 | return True 162 | # Reinitialize proxy 163 | self.proxy = bitcoin.rpc.Proxy('http://rt:rt@localhost:%d' % self.rpc_port) 164 | return False 165 | 166 | def wait_alive(self): 167 | """Wait for the node to become alive.""" 168 | while not self.is_alive(): 169 | if self.process.poll() is not None: 170 | self.print_log() 171 | raise Exception("Process terminated") 172 | time.sleep(0.1) 173 | 174 | def add_notify(self, command): 175 | """Add a command to be executed on block or wallet notify.""" 176 | with open(os.path.join(self.datadir, 'notify.sh'), 'a') as notify: 177 | notify.write(command) 178 | notify.write('\n') 179 | 180 | class LightningNode(object): 181 | """Interface to a lightningd instance.""" 182 | 183 | def __init__(self, bitcoind, datadir=None): 184 | self.bitcoind = bitcoind 185 | 186 | if datadir is None: 187 | self.datadir = tempfile.mkdtemp() 188 | else: 189 | os.mkdir(datadir, 0o700) 190 | self.datadir = datadir 191 | 192 | self.port = get_port() 193 | 194 | self.bitcoind.add_notify('%s $1 $2 %d' % (NOTIFY, self.port)) 195 | 196 | with open(os.path.join(self.datadir, 'lightning.conf'), 'w') as conf: 197 | conf.write("regtest=1\n") 198 | conf.write("rpcuser=rt\n") 199 | conf.write("rpcpassword=rt\n") 200 | conf.write("port=%d\n" % self.port) 201 | conf.write("bituser=rt\n") 202 | conf.write("bitpass=rt\n") 203 | conf.write("bitport=%d\n" % self.bitcoind.rpc_port) 204 | 205 | self.logfile, self.process, self.proxy = None, None, None 206 | self.start() 207 | 208 | def start(self): 209 | """Start the node.""" 210 | self.logfile = open(os.path.join(self.datadir, 'lightning.log'), 'w') 211 | self.process = subprocess.Popen( 212 | [ 213 | LIGHTNINGD, '-datadir=%s' % self.datadir, '-nodebug' 214 | ], 215 | stdin=subprocess.DEVNULL, 216 | stdout=self.logfile, 217 | stderr=subprocess.STDOUT) 218 | 219 | self.proxy = jsonrpcproxy.AuthProxy( 220 | 'http://localhost:%d/local/' % self.port, 221 | ('rt', 'rt')) 222 | 223 | def stop(self, cleanup=False): 224 | """Kill lightningd.""" 225 | try: 226 | self.process.kill() 227 | except ProcessLookupError: 228 | pass 229 | self.logfile.close() 230 | if cleanup: 231 | self.cleanup() 232 | 233 | def cleanup(self): 234 | """Remove the files.""" 235 | shutil.rmtree(self.datadir, ignore_errors=True) 236 | 237 | @contextmanager 238 | def paused(self): 239 | """Context manager to pause a node.""" 240 | self.stop(cleanup=False) 241 | yield 242 | self.start() 243 | self.wait_alive() 244 | 245 | def print_log(self): 246 | """Print the log file.""" 247 | with open(os.path.join(self.datadir, 'lightning.log')) as log: 248 | print(log.read()) 249 | 250 | def is_alive(self): 251 | """Check if the node is alive.""" 252 | try: 253 | return self.proxy.alive() 254 | except requests.exceptions.ConnectionError: 255 | pass 256 | # Reinitialize proxy 257 | self.proxy = jsonrpcproxy.AuthProxy( 258 | 'http://localhost:%d/local/' % self.port, 259 | ('rt', 'rt')) 260 | return False 261 | 262 | def wait_alive(self): 263 | """Wait for the node to become alive.""" 264 | while not self.is_alive(): 265 | if self.process.poll() is not None: 266 | self.print_log() 267 | raise Exception("Process terminated") 268 | time.sleep(0.1) 269 | 270 | class FullNode(object): 271 | """Combined Lightning and Bitcoin node.""" 272 | 273 | def __init__(self, datadir=None, cache=None, peers=None): 274 | if peers is None: 275 | peers = [] 276 | self.bitcoin = BitcoinNode(datadir, cache=cache, 277 | peers=[peer.bitcoin for peer in peers]) 278 | self.lightning = LightningNode(self.bitcoin, 279 | os.path.join(self.bitcoin.datadir, 'lightning')) 280 | 281 | def start(self): 282 | """Start the node.""" 283 | self.bitcoin.start() 284 | self.lightning.start() 285 | 286 | @property 287 | def bit(self): 288 | """Bitcoin proxy""" 289 | return self.bitcoin.proxy 290 | 291 | @property 292 | def lit(self): 293 | """Lightning proxy""" 294 | return self.lightning.proxy 295 | 296 | @property 297 | def lurl(self): 298 | """Lightning url""" 299 | return 'http://localhost:%d/' % self.lightning.port 300 | 301 | def stop(self, hard=False, cleanup=False): 302 | """Stop the node.""" 303 | self.lightning.stop(cleanup=False) 304 | self.bitcoin.stop(hard, cleanup=False) 305 | if cleanup: 306 | self.cleanup() 307 | 308 | def cleanup(self): 309 | """Remove files.""" 310 | self.lightning.cleanup() 311 | self.bitcoin.cleanup() 312 | 313 | @contextmanager 314 | def paused(self): 315 | """Context manager to pause a node.""" 316 | self.stop(hard=False, cleanup=False) 317 | yield 318 | self.start() 319 | self.wait_alive() 320 | 321 | def print_log(self, bit=True, lit=True): 322 | """Print logs.""" 323 | if bit: 324 | self.bitcoin.print_log() 325 | if lit: 326 | self.lightning.print_log() 327 | 328 | def cache(self): 329 | """Return a cache object.""" 330 | return self.bitcoin.cache() 331 | 332 | def sync_state(self): 333 | """Compare for synchronization across the network.""" 334 | return self.bitcoin.sync_state() 335 | 336 | def generate(self, blocks=1): 337 | """Generate blocks.""" 338 | self.bitcoin.generate(blocks) 339 | 340 | def is_alive(self): 341 | """Test if the node is alive.""" 342 | return self.bitcoin.is_alive() and self.lightning.is_alive() 343 | 344 | def wait_alive(self): 345 | """Wait for the node to be alive.""" 346 | self.bitcoin.wait_alive() 347 | self.lightning.wait_alive() 348 | 349 | class RegtestNetwork(object): 350 | """Regtest network.""" 351 | 352 | class Cache(object): # pylint: disable=too-few-public-methods 353 | """Container for miner and node caches.""" 354 | 355 | def __init__(self, miner_cache, node_caches): 356 | self.miner_cache = miner_cache 357 | self.node_caches = node_caches 358 | 359 | def cleanup(self): 360 | """Clean up miner and node caches.""" 361 | self.miner_cache.cleanup() 362 | for node_cache in self.node_caches: 363 | node_cache.cleanup() 364 | 365 | def __enter__(self): 366 | pass 367 | 368 | def __exit__(self, dummy_type, dummy_value, dummy_traceback): 369 | self.cleanup() 370 | 371 | def __init__(self, Node, degree=3, datadir=None, cache=None): 372 | if datadir is None: 373 | self.datadir = tempfile.mkdtemp() 374 | else: 375 | os.mkdir(datadir, 0o700) 376 | self.datadir = datadir 377 | if cache is None: 378 | cache = self.Cache(None, tuple(None for i in range(degree))) 379 | assert len(cache.node_caches) == degree 380 | self.miner = Node(os.path.join(self.datadir, 'miner'), cache=cache.miner_cache) 381 | self.nodes = [Node(os.path.join(self.datadir, 'node%d' % i), 382 | cache=node_cache, peers=[self.miner,]) 383 | for i, node_cache in zip(range(degree), cache.node_caches)] 384 | self.miner.wait_alive() 385 | for node in self.nodes: 386 | node.wait_alive() 387 | # sync nodes 388 | self.generate() 389 | 390 | def stop(self, hard=False, cleanup=False): 391 | """Stop all nodes.""" 392 | self.miner.stop(hard=hard, cleanup=False) 393 | for node in self.nodes: 394 | node.stop(hard=hard, cleanup=False) 395 | if cleanup: 396 | self.cleanup() 397 | 398 | def cleanup(self): 399 | """Remove files.""" 400 | self.miner.cleanup() 401 | for node in self.nodes: 402 | node.cleanup() 403 | shutil.rmtree(self.datadir, ignore_errors=True) 404 | 405 | def cache(self): 406 | """Return a cache object.""" 407 | return self.Cache(self.miner.cache(), tuple(node.cache() for node in self.nodes)) 408 | 409 | def print_log(self): 410 | """Print logs.""" 411 | print("Miner:") 412 | self.miner.print_log() 413 | for i, node in enumerate(self.nodes): 414 | print("Node %d:" % i) 415 | node.print_log() 416 | 417 | def sync(self, sleep_delay=0.1): 418 | """Synchronize the network.""" 419 | miner_state = self.miner.sync_state 420 | while any(node.sync_state() != miner_state for node in self.nodes 421 | if node.is_alive()): 422 | time.sleep(sleep_delay) 423 | miner_state = self.miner.sync_state() 424 | 425 | def generate(self, count=1): 426 | """Generate blocks.""" 427 | self.sync() 428 | self.miner.generate(count) 429 | self.sync() 430 | 431 | def __getitem__(self, index): 432 | return self.nodes[index] 433 | 434 | def make_cache(): 435 | """Cache the network after generating initial blocks.""" 436 | network = RegtestNetwork(Node=BitcoinNode) 437 | network.generate(101) 438 | network.miner.proxy.sendmany( 439 | "", 440 | {network[0].proxy.getnewaddress(): 100000000, 441 | network[1].proxy.getnewaddress(): 100000000, 442 | network[2].proxy.getnewaddress(): 200000000, 443 | }) 444 | network.sync() 445 | network.stop(hard=False, cleanup=False) 446 | cache = network.cache() 447 | network.cleanup() 448 | return cache 449 | 450 | def create(cache=None, datadir=os.path.abspath('regnet')): 451 | """Create a Lightning network.""" 452 | network = RegtestNetwork(Node=FullNode, cache=cache, datadir=datadir) 453 | return network 454 | 455 | def stop(network): 456 | """Stop a network""" 457 | network.stop(hard=False, cleanup=True) 458 | 459 | def kill(pid_path): 460 | """Stop a process specified in a pid file.""" 461 | if not os.path.isfile(pid_path): 462 | print(pid_path, "Not found") 463 | else: 464 | with open(pid_path) as pid_file: 465 | pid = int(pid_file.read()) 466 | try: 467 | os.kill(pid, signal.SIGKILL) 468 | except ProcessLookupError: 469 | pass 470 | 471 | def teardown(datadir=os.path.abspath('regnet')): 472 | """Clean up a forgotten FullNode network.""" 473 | for node in os.listdir(datadir): 474 | node_dir = os.path.join(datadir, node) 475 | assert os.path.isdir(node_dir) 476 | 477 | kill(os.path.join(node_dir, 'regtest', 'bitcoind.pid')) 478 | kill(os.path.join(node_dir, 'lightning', 'lightning.pid')) 479 | 480 | shutil.rmtree(datadir, ignore_errors=True) 481 | 482 | if __name__ == '__main__': 483 | pass 484 | -------------------------------------------------------------------------------- /test/test_integration.py: -------------------------------------------------------------------------------- 1 | """Integration testing.""" 2 | 3 | import unittest 4 | import time 5 | from test import regnet 6 | 7 | class TestChannel(unittest.TestCase): 8 | """Run basic tests on payment channels.""" 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.cache = regnet.make_cache() 13 | 14 | @classmethod 15 | def tearDownClass(cls): 16 | cls.cache.cleanup() 17 | 18 | def propagate(self): 19 | """Ensure all nodes up to date.""" 20 | self.net.generate() 21 | 22 | def setUp(self): 23 | # Set up 3 nodes: Alice, Bob, and Carol 24 | self.net = regnet.create(datadir=None, cache=self.cache) 25 | self.alice, self.bob, self.carol = self.net[0], self.net[1], self.net[2] 26 | # self.alice.bit is an interface to bitcoind, 27 | # self.alice.lit talks to the lightning node 28 | # self.alice.lurl is Alice's identifier 29 | 30 | def tearDown(self): 31 | self.net.stop(hard=True, cleanup=True) 32 | 33 | def test_setup(self): 34 | """Test that the setup worked.""" 35 | # Alice and Bob each start with 1.00 BTC 36 | self.assertEqual(self.alice.bit.getbalance(), 100000000) 37 | self.assertEqual(self.bob.bit.getbalance(), 100000000) 38 | 39 | def test_basic(self): 40 | """Test basic operation of a payment channel.""" 41 | # Open a channel between Alice and Bob 42 | self.alice.lit.create(self.bob.lurl, 50000000, 25000000) 43 | self.propagate() 44 | # There are some fees associated with opening a channel 45 | afee = 50000000 - self.alice.bit.getbalance() 46 | bfee = 75000000 - self.bob.bit.getbalance() 47 | self.assertGreaterEqual(afee, 0) 48 | self.assertGreaterEqual(bfee, 0) 49 | # (Balance) Alice: 0.50 BTC, Bob: 0.25 BTC 50 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 50000000) 51 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 25000000) 52 | # Bob sends Alice 0.05 BTC 53 | self.bob.lit.send(self.alice.lurl, 5000000) 54 | # (Balance) Alice: 0.55 BTC, Bob: 0.20 BTC 55 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 55000000) 56 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 20000000) 57 | # Now Alice sends Bob 0.10 BTC 58 | self.alice.lit.send(self.bob.lurl, 10000000) 59 | # (Balance) Alice: 0.45 BTC, Bob: 0.30 BTC 60 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 45000000) 61 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 30000000) 62 | # Bob closes the channel 63 | self.bob.lit.close(self.alice.lurl) 64 | self.propagate() 65 | # The Lightning balance is returned to the bitcoin wallet 66 | # If any coin was held for fees which were never paid, 67 | # they are refunded, so the balance may be more than expected. 68 | self.assertGreaterEqual(self.alice.bit.getbalance(), 95000000 - afee) 69 | self.assertGreaterEqual(self.bob.bit.getbalance(), 105000000 - bfee) 70 | 71 | def test_stress(self): 72 | """Test edge cases in payment channels.""" 73 | # Open *two* payment channels Bob - Alice - Carol 74 | self.alice.lit.create(self.bob.lurl, 25000000, 50000000) 75 | self.propagate() 76 | self.carol.lit.create(self.alice.lurl, 50000000, 25000000) 77 | self.propagate() 78 | # Account for fees 79 | afee = 50000000 - self.alice.bit.getbalance() 80 | bfee = 50000000 - self.bob.bit.getbalance() 81 | self.assertGreaterEqual(afee, 0) 82 | self.assertGreaterEqual(bfee, 0) 83 | # Balance (A-C) Alice: 0.25 BTC, Carol: 0.50 BTC 84 | # Balance (B-A) Bob: 0.50 BTC, Alice: 0.25 BTC 85 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 25000000) 86 | self.assertEqual(self.alice.lit.getbalance(self.carol.lurl), 25000000) 87 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 50000000) 88 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 50000000) 89 | # Carol sends 0.25 BTC to Alice 90 | self.carol.lit.send(self.alice.lurl, 25000000) 91 | # Balance (A-C) Alice: 0.50 BTC, Carol: 0.25 BTC 92 | self.assertEqual(self.alice.lit.getbalance(self.carol.lurl), 50000000) 93 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 25000000) 94 | # Alice sends 0.15 BTC to Carol 95 | self.alice.lit.send(self.carol.lurl, 15000000) 96 | # Balance (A-C) Alice: 0.35 BTC, Carol: 0.40 BTC 97 | self.assertEqual(self.alice.lit.getbalance(self.carol.lurl), 35000000) 98 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 40000000) 99 | # Bob sends Alice 0.50 BTC (his whole balance) 100 | self.bob.lit.send(self.alice.lurl, 50000000) 101 | # Balance (B-A) Bob: 0.00 BTC, Alice: 0.75 BTC 102 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 75000000) 103 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 0) 104 | # Alice sends Bob 0.75 BTC (her whole balance) 105 | self.alice.lit.send(self.bob.lurl, 75000000) 106 | # Balance (B-A) Bob: 0.75 BTC, Alice: 0.00 BTC 107 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 0) 108 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 75000000) 109 | # Alice closes the channel with Bob, on an empty account (Alice opened) 110 | self.alice.lit.close(self.bob.lurl) 111 | self.propagate() 112 | self.assertGreaterEqual(self.alice.bit.getbalance(), 50000000 - afee) 113 | self.assertGreaterEqual(self.bob.bit.getbalance(), 125000000 - bfee) 114 | # Alice closes the channel with Carol (Carol opened) 115 | self.alice.lit.close(self.carol.lurl) 116 | self.propagate() 117 | self.assertGreaterEqual(self.alice.bit.getbalance(), 85000000 - afee) 118 | 119 | def test_unilateral_close(self): 120 | """Test unilateral close.""" 121 | # Set up channel between Alice and Bob 122 | self.alice.lit.create(self.bob.lurl, 50000000, 25000000) 123 | self.propagate() 124 | afee = 50000000 - self.alice.bit.getbalance() 125 | bfee = 75000000 - self.bob.bit.getbalance() 126 | self.assertGreaterEqual(afee, 0) 127 | self.assertGreaterEqual(bfee, 0) 128 | # Do some transactions 129 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 50000000) 130 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 25000000) 131 | self.bob.lit.send(self.alice.lurl, 5000000) 132 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 55000000) 133 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 20000000) 134 | # Pause Bob 135 | with self.bob.paused(): 136 | # Publish Alice's commitment transactions 137 | commitment = self.alice.lit.getcommitmenttransactions(self.bob.lurl) 138 | for transaction in commitment: 139 | self.alice.bit.sendrawtransaction(transaction) 140 | self.propagate() 141 | time.sleep(1) 142 | self.propagate() 143 | self.propagate() 144 | # Alice and Bob get their money out 145 | self.assertGreaterEqual(self.bob.bit.getbalance(), 95000000 - bfee) 146 | self.assertGreaterEqual(self.alice.bit.getbalance(), 105000000 - afee) 147 | 148 | @unittest.expectedFailure 149 | def test_revoked(self): 150 | """Test a revoked commitment transaction being published.""" 151 | # Set up channel between Alice and Bob 152 | self.alice.lit.create(self.bob.lurl, 50000000, 25000000) 153 | self.propagate() 154 | afee = 50000000 - self.alice.bit.getbalance() 155 | bfee = 75000000 - self.bob.bit.getbalance() 156 | self.assertGreaterEqual(afee, 0) 157 | self.assertGreaterEqual(bfee, 0) 158 | # Make a transaction 159 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 50000000) 160 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 25000000) 161 | self.bob.lit.send(self.alice.lurl, 5000000) 162 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 55000000) 163 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 20000000) 164 | # Save Alice's old commitment transactions 165 | commitment = self.alice.lit.getcommitmenttransactions(self.bob.lurl) 166 | # Do annother transaction, Alice sends Bob money 167 | self.alice.lit.send(self.bob.lurl, 10000000) 168 | self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 45000000) 169 | self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 30000000) 170 | # Alice publishes her old, revoked commitment transactions 171 | for transaction in commitment: 172 | self.alice.bit.sendrawtransaction(transaction) 173 | self.propagate() 174 | time.sleep(1) 175 | self.propagate() 176 | # Bob ends up with all the money 177 | self.assertGreaterEqual(self.bob.bit.getbalance(), 150000000 - bfee) 178 | 179 | class TestLightning(unittest.TestCase): 180 | """Run basic tests on payment channels.""" 181 | 182 | @classmethod 183 | def setUpClass(cls): 184 | cls.cache = regnet.make_cache() 185 | 186 | @classmethod 187 | def tearDownClass(cls): 188 | cls.cache.cleanup() 189 | 190 | def propagate(self): 191 | """Ensure all nodes up to date.""" 192 | self.net.generate() 193 | 194 | def setUp(self): 195 | # As in TestChannel, set up 3 nodes 196 | self.net = regnet.create(datadir=None, cache=self.cache) 197 | self.alice, self.bob, self.carol = self.net[0], self.net[1], self.net[2] 198 | # Set up channels between so the network is Alice - Carol - Bob 199 | self.alice.lit.create(self.carol.lurl, 50000000, 50000000) 200 | self.propagate() 201 | self.bob.lit.create(self.carol.lurl, 50000000, 50000000) 202 | self.propagate() 203 | 204 | def tearDown(self): 205 | self.net.stop(hard=True, cleanup=True) 206 | 207 | def test_setup(self): 208 | """Test that the setup worked.""" 209 | # (Balance) Alice-Carol: Alice: 0.50 BTC, Carol 0.50 BTC 210 | # Carol-Bob : Carol: 0.50 BTC, Bob 0.50 BTC 211 | # (Total) Alice: 0.50 BTC, Carol: 1.00 BTC, Bob: 0.50 BTC 212 | self.assertEqual(self.alice.lit.getbalance(self.carol.lurl), 50000000) 213 | self.assertEqual(self.bob.lit.getbalance(self.carol.lurl), 50000000) 214 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 50000000) 215 | self.assertEqual(self.carol.lit.getbalance(self.bob.lurl), 50000000) 216 | 217 | def test_payment(self): 218 | """Test multi-hop payment.""" 219 | # Note Alice and Bob do not have a payment channel open directly. 220 | # They are connected through Carol 221 | self.alice.lit.send(self.bob.lurl, 5000000) 222 | # There is a fee associated with multi-hop payments 223 | fee = 45000000 - self.alice.lit.getbalance(self.carol.lurl) 224 | self.assertGreaterEqual(fee, 0) 225 | # (Balance) Alice-Carol: Alice: 0.45 - fee BTC, Carol 0.55 + fee BTC 226 | # Carol-Bob : Carol: 0.45 BTC, Bob 0.55 BTC 227 | # (Total) Alice: 0.45 - fee BTC, Carol: 1.00 + fee BTC, Bob: 0.55 BTC 228 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 55000000 + fee) 229 | self.assertEqual(self.bob.lit.getbalance(self.carol.lurl), 55000000) 230 | self.assertEqual(self.carol.lit.getbalance(self.bob.lurl), 45000000) 231 | # Send money the other direction 232 | self.bob.lit.send(self.alice.lurl, 10000000) 233 | # Annother fee will be deducted 234 | fee2 = 45000000 - self.bob.lit.getbalance(self.carol.lurl) 235 | self.assertGreaterEqual(fee2, 0) 236 | # (Balance) Alice-Carol: Alice: 0.55 - fee BTC, Carol 0.45 + fee BTC 237 | # Carol-Bob : Carol: 0.55 + fee2 BTC, Bob 0.45 - fee2 BTC 238 | # (Total) Alice: 0.55 - fee BTC, Carol: 1.00 + fee + fee2 BTC, Bob: 0.45 - fee2 BTC 239 | self.assertEqual(self.carol.lit.getbalance(self.alice.lurl), 45000000 + fee) 240 | self.assertEqual(self.alice.lit.getbalance(self.carol.lurl), 55000000 - fee) 241 | self.assertEqual(self.carol.lit.getbalance(self.bob.lurl), 55000000 + fee2) 242 | 243 | @unittest.expectedFailure 244 | def test_route_close(self): 245 | """Test routing around closed channels.""" 246 | # Create a new channel between Alice and Bob 247 | # so all 3 are connected to each other. 248 | self.alice.lit.create(self.bob.lurl, 25000000, 25000000) 249 | # Close the connection between Alice and Carol 250 | self.alice.lit.close(self.carol.lurl) 251 | self.propagate() 252 | # Alice sends 0.10 BTC to Carol 253 | self.alice.lit.send(self.carol.lurl, 10000000) 254 | # Carol should have recieved money from Bob 255 | self.assertEqual(self.carol.lit.getbalance(self.bob.lurl), 60000000) 256 | 257 | if __name__ == '__main__': 258 | unittest.main() 259 | -------------------------------------------------------------------------------- /test/test_jsonrpcproxy.py: -------------------------------------------------------------------------------- 1 | """Tests for jsonrpcproxy.py.""" 2 | 3 | import unittest 4 | import bitcoin 5 | from bitcoin.core.script import CScript 6 | from bitcoin.base58 import CBase58Data 7 | from bitcoin.wallet import P2SHBitcoinAddress, P2PKHBitcoinAddress 8 | from bitcoin.core import CMutableTransaction, CMutableTxIn, CMutableTxOut 9 | from bitcoin.core import COutPoint 10 | from jsonrpcproxy import to_json, from_json, SmartDispatcher, Proxy 11 | bitcoin.SelectParams('regtest') 12 | 13 | class TestTranslation(unittest.TestCase): 14 | def test_json_roundtrip(self): 15 | VALUES = [ 16 | 42, 0, -42, 2100000000000000, -2100000000000000, 17 | "basic string", "\u1111Unicode", "\U00010000Wide Unicode", 18 | "\x00\n\t\r\nEscape codes", "\"'\"Quotes", "", 19 | None, 20 | b"\x00\x01\xFFBinary data", b"", 21 | CBase58Data.from_bytes(b'\x00\x01\xFF', 42), 22 | P2SHBitcoinAddress.from_bytes(b'\x00\x01\xFF'), 23 | P2PKHBitcoinAddress.from_bytes(b'\x00\x01\xFF'), 24 | CMutableTxIn(COutPoint(b'\x00'*16+b'\xFF'*16, 42), 25 | CScript(b'\x00\x01\xFF'), 26 | 42), 27 | CMutableTxOut(42, CScript(b'\x00\x01\xFF')), 28 | CMutableTransaction([CMutableTxIn(COutPoint(b'\x00'*32, 42), 29 | CScript(b'\x00\x01\xFF'), 30 | 42), 31 | CMutableTxIn(COutPoint(b'\xFF'*32, 42), 32 | CScript(b'\xFF\x01\x00'), 33 | 43)], 34 | [CMutableTxOut(42, CScript(b'\x00\x01\xFF')), 35 | CMutableTxOut(43, CScript(b'\xFF\x01\x00'))], 36 | 42, 3), 37 | [1, b'\x00\x01\xFF', "List Test",], 38 | {'a':1, 'key':b'\xFF\x01\x00', 1:'Dictionary Test'}, 39 | [{3: [0, 1, 2,],}, [[b'\xFFRecursion Test',],],], 40 | ] 41 | for value in VALUES: 42 | self.assertEqual(from_json(to_json(value)), value) 43 | 44 | def test_None_hiding(self): 45 | # Our jsonrpc server library gets confused when functions return None 46 | self.assertNotEqual(to_json(None), None) 47 | self.assertEqual(from_json(to_json(None)), None) 48 | 49 | def test_CBase58Data_version(self): 50 | self.assertEqual(from_json(to_json( 51 | CBase58Data.from_bytes(b'\x00\x01\xFF', 42))).nVersion, 52 | 42) 53 | 54 | def test_tuple(self): 55 | value = (1, 'a', b'b',) 56 | self.assertEqual(from_json(to_json(value)), list(value)) 57 | 58 | class TestDispatcher(unittest.TestCase): 59 | def setUp(self): 60 | self.dispatcher = SmartDispatcher() 61 | self.add = self.dispatcher.add_method 62 | 63 | def test_echo(self): 64 | self.add(lambda x:x, 'echo') 65 | echo = self.dispatcher['echo'] 66 | VALUES = [ 67 | 42, "str", b"\x00\x01\xFFbytes", 68 | {1: b"dict",}, [1, "list", b"bytes",], 69 | ] 70 | for value in VALUES: 71 | self.assertEqual(from_json(echo(to_json(value))), value) 72 | 73 | def test_type(self): 74 | self.add(lambda x:str(type(x)), 'type') 75 | typeof = self.dispatcher['type'] 76 | VALUES = [ 77 | 42, "str", b"\x00\x01\xFFbytes", 78 | {1: b"dict",}, [1, "list", b"bytes",], 79 | ] 80 | for value in VALUES: 81 | self.assertEqual(from_json(typeof(to_json(value))), 82 | str(type(value))) 83 | 84 | def test_error(self): 85 | class TestException(Exception): 86 | pass 87 | @self.add 88 | def error(args): 89 | raise TestException(*args) 90 | error = self.dispatcher['error'] 91 | VALUES = [ 92 | ('ordinary', 1, 2, 'foo',), 93 | ('bytes', b'\x00\x01\xFF',), 94 | ('list', [1, 'a', b'b',],), 95 | ] 96 | for value in VALUES: 97 | self.assertRaises(TestException, error, to_json(value)) 98 | 99 | @self.add 100 | def nested_error(): 101 | raise TestException('nested', TestException('2nd layer')) 102 | self.assertRaises(TestException, self.dispatcher['nested_error']) 103 | --------------------------------------------------------------------------------