├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bech32.py ├── bin └── .keep ├── btcproxy.py ├── cli.py ├── conftest.py ├── eclair.py ├── fixtures.py ├── google └── api │ ├── annotations_pb2.py │ ├── annotations_pb2_grpc.py │ ├── http_pb2.py │ └── http_pb2_grpc.py ├── lightningd.py ├── lnaddr.py ├── lnd.py ├── logback.xml ├── output └── .gitkeep ├── ptarmd.py ├── pytest.ini ├── reports ├── 07f1821c5629d6eaf2df351ce27fc9c30cdd4fb0d3e8fb6fe76ffe1194bc28b7.json ├── 1ab19ffde3f4e0329c86c2b3513b74538a47282711b908f7e7d21cd83073193b.json ├── 2d1967e0d3fbe290e65602cec671dd9fd824faba2355f1949b0dcf94fd2d1718.json ├── 55e9c14d4944c5c0277885eaa04c2ecb6aab85d6a70e94885bbbec2fec7c9851.json ├── 732bcbb1139cb0b5a45c18cc6564dc0113481a48029606e9991c9c77125917d5.json ├── 7737312c288a26952607361202f17f72330b3177a3a8d10231d6cb00b93beb18.json ├── 7f4427116da10d17f4139438aff52a59da3670eedfe0dc0161ccd741606b6f30.json ├── 8100d3ac5b67d271b485476cd7e6153e9776ed341201433eabc285dc1abc212a.json ├── 8f69ab048dc589e27ec64f658f2ead281d7a0a4665363a1b2ca567de6ce4524a.json ├── 9175bda8aa50d37257d21d582f979cef414668457079f3ead0317d5abc3099d5.json ├── 97839c27a65bd75d0ae86e2cc98f6e8d4b27ba72e6338bcac8dd4c1307d0f9e0.json ├── baf39e469787390145836e360e94943772b46f58bff9642e91442fad3b2361f3.json ├── de3cbef6d61ec06e46743a4ddfe691b85aba86d0172ec2ac1be4445aed6b62da.json ├── e05363fe71cb53feee76d05abf414da920bb3cf28060941026a128a90af0c87b.json ├── ece062dbbb45b913af4c287b4c3a77d46bd722f10c7771eb74516249e113c360.json └── f5fa74e3adb19c56e191ae70d7a4d233e09f8863006870da7f9590b7ab03c1e0.json ├── requirements.txt ├── rpc_pb2.py ├── rpc_pb2_grpc.py ├── templates ├── _base.html ├── _report.html └── index.html ├── test.py ├── tls.cert ├── tls.key └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | dist 3 | *.pyc 4 | bin/* 5 | src/* 6 | templates/*.json 7 | output 8 | report.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | services: docker 3 | 4 | install: 5 | - make docker-build 6 | 7 | after_success: 8 | # Log that the build worked, because we all need some good news 9 | - echo "Success running lightning-integration" 10 | 11 | jobs: 12 | include: 13 | 14 | - name: 'Build eclair' 15 | script: 16 | - docker run lnintegration bash -c "make update-eclair bin/eclair.jar && py.test -v test.py -k EclairNode" 17 | 18 | - name: 'Build clightning' 19 | script: 20 | - docker run lnintegration bash -c "make update-clightning bin/lightningd && py.test -v test.py -k LightningNode" 21 | 22 | - name: 'Build lnd' 23 | script: 24 | - docker run lnintegration bash -c "make update-lnd bin/lnd && py.test -v test.py -k LndNode" 25 | 26 | - name: 'Build ptarmigan' 27 | script: 28 | - docker run lnintegration bash -c "make update-ptarmigan bin/ptarmd && py.test -v test.py -k PtarmNode" 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y software-properties-common \ 5 | && add-apt-repository ppa:bitcoin/bitcoin \ 6 | && apt-get update \ 7 | && apt-get install -y \ 8 | autoconf \ 9 | automake \ 10 | autotools-dev \ 11 | bc \ 12 | bsdmainutils \ 13 | build-essential \ 14 | clang \ 15 | curl \ 16 | gettext \ 17 | git \ 18 | jq \ 19 | libboost-all-dev \ 20 | wget \ 21 | libcurl4-openssl-dev \ 22 | libdb4.8++-dev \ 23 | libdb4.8-dev \ 24 | libev-dev \ 25 | libevent-dev \ 26 | libgmp-dev \ 27 | libjansson-dev \ 28 | libsecp256k1-dev \ 29 | libsodium-dev \ 30 | libsqlite3-dev \ 31 | libssl-dev \ 32 | libtool \ 33 | libzmq3-dev \ 34 | miniupnpc \ 35 | net-tools \ 36 | openjdk-11-jdk \ 37 | pkg-config \ 38 | python \ 39 | python3 \ 40 | python3-mako \ 41 | python3-pip \ 42 | zlib1g-dev \ 43 | && rm -rf /var/lib/apt/lists/* 44 | 45 | ARG BITCOIN_VERSION=0.17.1 46 | ENV BITCOIN_TARBALL bitcoin-$BITCOIN_VERSION-x86_64-linux-gnu.tar.gz 47 | ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/$BITCOIN_TARBALL 48 | ENV BITCOIN_ASC_URL https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/SHA256SUMS.asc 49 | ENV BITCOIN_PGP_KEY 01EA5486DE18A882D4C2684590C8019E36C2E964 50 | 51 | RUN cd /tmp \ 52 | && wget -qO $BITCOIN_TARBALL "$BITCOIN_URL" \ 53 | && gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "$BITCOIN_PGP_KEY" \ 54 | && wget -qO bitcoin.asc "$BITCOIN_ASC_URL" \ 55 | && gpg --verify bitcoin.asc \ 56 | && grep $BITCOIN_TARBALL bitcoin.asc | tee SHA256SUMS.asc \ 57 | && sha256sum -c SHA256SUMS.asc \ 58 | && BD=bitcoin-$BITCOIN_VERSION/bin \ 59 | && tar -xzvf $BITCOIN_TARBALL \ 60 | && cp bitcoin-$BITCOIN_VERSION/bin/bitcoin* /usr/bin/ \ 61 | && rm -rf $BITCOIN_TARBALL bitcoin-$BITCOIN_VERSION 62 | 63 | # maven for java builds (eclair) 64 | RUN cd /tmp \ 65 | && wget -qO mvn.tar.gz https://www-us.apache.org/dist/maven/maven-3/3.6.2/binaries/apache-maven-3.6.2-bin.tar.gz \ 66 | && tar -xzf mvn.tar.gz \ 67 | && rm mvn.tar.gz \ 68 | && mv apache-maven-3.6.2 /usr/local/maven \ 69 | && ln -s /usr/local/maven/bin/mvn /usr/local/bin 70 | 71 | RUN cd /tmp \ 72 | && wget -q https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz \ 73 | && tar -xf go1.12.7.linux-amd64.tar.gz \ 74 | && mv go /usr/local \ 75 | && rm go1.12.7.linux-amd64.tar.gz \ 76 | && ln -s /usr/local/go/bin/go /usr/bin/ 77 | 78 | ENV GOROOT=/usr/local/go 79 | 80 | VOLUME /root/lightning-integration/reports 81 | VOLUME /root/lightning-integration/output 82 | 83 | WORKDIR /root/lightning-integration 84 | 85 | # lightning-integration 86 | COPY requirements.txt /root/lightning-integration/requirements.txt 87 | RUN ln -sf /usr/bin/python3 /usr/bin/python \ 88 | && ln -sf /usr/bin/pip3 /usr/bin/pip \ 89 | && pip install -r /root/lightning-integration/requirements.txt 90 | 91 | ENV LC_ALL C.UTF-8 92 | ENV LANG C.UTF-8 93 | ENV TEST_DEBUG=0 94 | 95 | COPY . /root/lightning-integration/ 96 | CMD ["make", "update", "clients", "test"] 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Note: the modules in the ccan/ directory have their own licenses, but 2 | the rest of the code is covered by the following (BSD-MIT) license: 3 | 4 | Copyright Christian Decker (Blockstream) 2017-2019. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTEST_OPTS=--timeout=600 --timeout-method=thread -v -p no:logging 2 | ifneq ($(PYTEST_PAR),) 3 | PYTEST_OPTS += -n=$(PYTEST_PAR) 4 | endif 5 | 6 | NPROC = 4 7 | OS=$(shell uname -s) 8 | 9 | ifeq ($(OS),Linux) 10 | NPROC=$(shell grep -c ^processor /proc/cpuinfo) 11 | endif 12 | 13 | PWD = $(shell pwd) 14 | GO111MODULE = on 15 | 16 | src/eclair: 17 | git clone https://github.com/ACINQ/eclair.git src/eclair 18 | 19 | src/lightning: 20 | git clone --recurse-submodules https://github.com/ElementsProject/lightning.git src/lightning 21 | 22 | src/lnd: 23 | git clone https://github.com/lightningnetwork/lnd src/lnd 24 | 25 | src/ptarmigan: 26 | git clone https://github.com/nayutaco/ptarmigan.git src/ptarmigan 27 | 28 | update-eclair: src/eclair 29 | rm src/eclair/version || true 30 | cd src/eclair && git stash; git pull origin master 31 | 32 | update-clightning: src/lightning 33 | rm src/lightning/version || true 34 | cd src/lightning && git stash; git pull origin master 35 | 36 | update-lnd: src/lnd 37 | rm src/lnd/version || true 38 | cd src/lnd && git stash; git pull origin master 39 | 40 | update-ptarmigan: src/ptarmigan 41 | rm src/ptarmigan/version || true 42 | cd src/ptarmigan && git stash; git pull origin master 43 | 44 | update: update-eclair update-clightning update-lnd update-ptarmigan 45 | 46 | bin/eclair.jar: src/eclair 47 | (cd src/eclair; git rev-parse HEAD) > src/eclair/version 48 | (cd src/eclair/; mvn -T ${NPROC} package -Dmaven.test.skip=true || true) 49 | cp src/eclair/eclair-node/target/eclair-node-*-$(shell git --git-dir=src/eclair/.git rev-parse HEAD | cut -b 1-7).jar bin/eclair.jar 50 | 51 | bin/lightningd: src/lightning 52 | (cd src/lightning; git rev-parse HEAD) > src/lightning/version 53 | cd src/lightning; ./configure --enable-developer --disable-valgrind && make CC=clang -j${NPROC} 54 | cp src/lightning/lightningd/lightningd src/lightning/lightningd/lightning_* bin 55 | 56 | bin/ptarmd: src/ptarmigan 57 | (cd src/ptarmigan; git rev-parse HEAD) > src/ptarmigan/version 58 | cd src/ptarmigan; sed -i -e "s/ENABLE_DEVELOPER_MODE=0/ENABLE_DEVELOPER_MODE=1/g" options.mak 59 | cd src/ptarmigan; sed -i -e "s/ENABLE_PLOG_TO_STDOUT_PTARMD=0/ENABLE_PLOG_TO_STDOUT_PTARMD=1/g" options.mak 60 | cd src/ptarmigan; make full 61 | cp src/ptarmigan/install/ptarmd bin 62 | cp src/ptarmigan/install/showdb bin 63 | cp src/ptarmigan/install/routing bin 64 | 65 | bin/lnd: src/lnd 66 | (cd src/lnd; git rev-parse HEAD) > src/lnd/version 67 | cd src/lnd \ 68 | && go mod vendor \ 69 | && go build -v -mod=vendor -o lnd cmd/lnd/main.go \ 70 | && go build -v -mod=vendor -o lncli github.com/lightningnetwork/lnd/cmd/lncli 71 | cp src/lnd/lnd src/lnd/lncli bin/ 72 | 73 | clean: 74 | rm src/lnd/version src/lightning/version src/eclair/version src/ptarmigan/version || true 75 | rm bin/* || true 76 | cd src/lightning; make clean 77 | cd src/eclair; mvn clean 78 | cd src/ptarmigan; make distclean 79 | 80 | clients: bin/lightningd bin/lnd bin/eclair.jar bin/ptarmd 81 | 82 | test: clients 83 | # Failure is always an option 84 | py.test -v test.py ${PYTEST_OPTS} --json=report.json || true 85 | python cli.py postprocess 86 | 87 | site: 88 | rm -rf output/*; rm templates/*.json || true 89 | cp reports/* templates/ 90 | python cli.py html 91 | 92 | push: 93 | cd output; \ 94 | git init;\ 95 | git config user.name "Travis CI";\ 96 | git config user.email "decker.christian+travis@gmail.com";\ 97 | git add .;\ 98 | git commit --quiet -m "Deploy to GitHub Pages";\ 99 | git push --force "git@github.com:cdecker/lightning-integration.git" master:gh-pages 100 | 101 | docker-build: 102 | docker build --tag=lnintegration . 103 | docker-run: docker-build 104 | docker run lnintegration 105 | builder: 106 | docker build -t cdecker/lightning-integration:latest - > 25 33 | chk = (chk & 0x1ffffff) << 5 ^ value 34 | for i in range(5): 35 | chk ^= generator[i] if ((top >> i) & 1) else 0 36 | return chk 37 | 38 | 39 | def bech32_hrp_expand(hrp): 40 | """Expand the HRP into values for checksum computation.""" 41 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 42 | 43 | 44 | def bech32_verify_checksum(hrp, data): 45 | """Verify a checksum given HRP and converted data characters.""" 46 | return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 47 | 48 | 49 | def bech32_create_checksum(hrp, data): 50 | """Compute the checksum values given HRP and data.""" 51 | values = bech32_hrp_expand(hrp) + data 52 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 53 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 54 | 55 | 56 | def bech32_encode(hrp, data): 57 | """Compute a Bech32 string given HRP and data values.""" 58 | combined = data + bech32_create_checksum(hrp, data) 59 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 60 | 61 | 62 | def bech32_decode(bech): 63 | """Validate a Bech32 string, and determine HRP and data.""" 64 | if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or 65 | (bech.lower() != bech and bech.upper() != bech)): 66 | return (None, None) 67 | bech = bech.lower() 68 | pos = bech.rfind('1') 69 | if pos < 1 or pos + 7 > len(bech): #or len(bech) > 90: 70 | return (None, None) 71 | if not all(x in CHARSET for x in bech[pos+1:]): 72 | return (None, None) 73 | hrp = bech[:pos] 74 | data = [CHARSET.find(x) for x in bech[pos+1:]] 75 | if not bech32_verify_checksum(hrp, data): 76 | return (None, None) 77 | return (hrp, data[:-6]) 78 | 79 | 80 | def convertbits(data, frombits, tobits, pad=True): 81 | """General power-of-2 base conversion.""" 82 | acc = 0 83 | bits = 0 84 | ret = [] 85 | maxv = (1 << tobits) - 1 86 | max_acc = (1 << (frombits + tobits - 1)) - 1 87 | for value in data: 88 | if value < 0 or (value >> frombits): 89 | return None 90 | acc = ((acc << frombits) | value) & max_acc 91 | bits += frombits 92 | while bits >= tobits: 93 | bits -= tobits 94 | ret.append((acc >> bits) & maxv) 95 | if pad: 96 | if bits: 97 | ret.append((acc << (tobits - bits)) & maxv) 98 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 99 | return None 100 | return ret 101 | 102 | 103 | def decode(hrp, addr): 104 | """Decode a segwit address.""" 105 | hrpgot, data = bech32_decode(addr) 106 | if hrpgot != hrp: 107 | return (None, None) 108 | decoded = convertbits(data[1:], 5, 8, False) 109 | if decoded is None or len(decoded) < 2 or len(decoded) > 40: 110 | return (None, None) 111 | if data[0] > 16: 112 | return (None, None) 113 | if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: 114 | return (None, None) 115 | return (data[0], decoded) 116 | 117 | 118 | def encode(hrp, witver, witprog): 119 | """Encode a segwit address.""" 120 | ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) 121 | assert decode(hrp, ret) is not (None, None) 122 | return ret 123 | 124 | -------------------------------------------------------------------------------- /bin/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdecker/lightning-integration/4e66f74f68b28efeac437b3e104fe00b75d73e05/bin/.keep -------------------------------------------------------------------------------- /btcproxy.py: -------------------------------------------------------------------------------- 1 | """ A bitcoind proxy that allows instrumentation and canned responses 2 | """ 3 | from flask import Flask, request 4 | from bitcoin.rpc import JSONRPCError 5 | from bitcoin.rpc import RawProxy as BitcoinProxy 6 | from utils import BitcoinD 7 | from cheroot.wsgi import Server 8 | from cheroot.wsgi import PathInfoDispatcher 9 | 10 | import decimal 11 | import flask 12 | import json 13 | import logging 14 | import os 15 | import threading 16 | 17 | 18 | class DecimalEncoder(json.JSONEncoder): 19 | """By default json.dumps does not handle Decimals correctly, so we override it's handling 20 | """ 21 | def default(self, o): 22 | if isinstance(o, decimal.Decimal): 23 | return float(o) 24 | return super(DecimalEncoder, self).default(o) 25 | 26 | 27 | class ProxiedBitcoinD(BitcoinD): 28 | def __init__(self, bitcoin_dir, proxyport=0): 29 | BitcoinD.__init__(self, bitcoin_dir, rpcport=None) 30 | self.app = Flask("BitcoindProxy") 31 | self.app.add_url_rule("/", "API entrypoint", self.proxy, methods=['POST']) 32 | self.proxyport = proxyport 33 | self.mocks = {} 34 | 35 | def _handle_request(self, r): 36 | conf_file = os.path.join(self.bitcoin_dir, 'bitcoin.conf') 37 | brpc = BitcoinProxy(btc_conf_file=conf_file) 38 | method = r['method'] 39 | 40 | # If we have set a mock for this method reply with that instead of 41 | # forwarding the request. 42 | if method in self.mocks and type(method) == dict: 43 | return self.mocks[method] 44 | elif method in self.mocks and callable(self.mocks[method]): 45 | return self.mocks[method](r) 46 | 47 | try: 48 | reply = { 49 | "result": brpc._call(r['method'], *r['params']), 50 | "error": None, 51 | "id": r['id'] 52 | } 53 | except JSONRPCError as e: 54 | reply = { 55 | "error": e.error, 56 | "id": r['id'] 57 | } 58 | return reply 59 | 60 | def proxy(self): 61 | r = json.loads(request.data.decode('ASCII')) 62 | 63 | if isinstance(r, list): 64 | reply = [self._handle_request(subreq) for subreq in r] 65 | else: 66 | reply = self._handle_request(r) 67 | 68 | reply = json.dumps(reply, cls=DecimalEncoder) 69 | logging.debug("Replying to %r with %r", r, reply) 70 | 71 | response = flask.Response(reply) 72 | response.headers['Content-Type'] = 'application/json' 73 | return response 74 | 75 | def start(self): 76 | d = PathInfoDispatcher({'/': self.app}) 77 | self.server = Server(('0.0.0.0', self.proxyport), d) 78 | self.proxy_thread = threading.Thread(target=self.server.start) 79 | self.proxy_thread.daemon = True 80 | self.proxy_thread.start() 81 | BitcoinD.start(self) 82 | 83 | # Now that bitcoind is running on the real rpcport, let's tell all 84 | # future callers to talk to the proxyport. We use the bind_addr as a 85 | # signal that the port is bound and accepting connections. 86 | while self.server.bind_addr[1] == 0: 87 | pass 88 | self.proxiedport = self.rpcport 89 | self.rpcport = self.server.bind_addr[1] 90 | logging.debug("bitcoind reverse proxy listening on {}, forwarding to {}".format( 91 | self.rpcport, self.proxiedport 92 | )) 93 | 94 | def stop(self): 95 | BitcoinD.stop(self) 96 | self.server.stop() 97 | self.proxy_thread.join() 98 | 99 | def mock_rpc(self, method, response=None): 100 | """Mock the response to a future RPC call of @method 101 | 102 | The response can either be a dict with the full JSON-RPC response, or a 103 | function that returns such a response. If the response is None the mock 104 | is removed and future calls will be passed through to bitcoind again. 105 | 106 | """ 107 | if response is not None: 108 | self.mocks[method] = response 109 | elif method in self.mocks: 110 | del self.mocks[method] 111 | 112 | 113 | # The main entrypoint is mainly used to test the proxy. It is not used during 114 | # lightningd testing. 115 | if __name__ == "__main__": 116 | p = ProxiedBitcoinD(bitcoin_dir='/tmp/bitcoind-test/', proxyport=5000) 117 | p.start() 118 | p.proxy_thread.join() 119 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from google.cloud import storage 3 | from hashlib import sha256 4 | from staticjinja import make_site 5 | 6 | import click 7 | import json 8 | import os 9 | import sys 10 | 11 | 12 | @click.group() 13 | def cli(): 14 | pass 15 | 16 | 17 | def die(msg): 18 | print(msg) 19 | sys.exit(1) 20 | 21 | 22 | def get_version(impl_name): 23 | fname = os.path.join('src', impl_name, 'version') 24 | if not os.path.exists(fname): 25 | die("Could not find version of implementation {}".format(impl_name)) 26 | return open(fname).read().strip() 27 | 28 | 29 | @click.command() 30 | def postprocess(): 31 | if not os.path.exists('report.json'): 32 | die("No report found to process") 33 | report = json.load(open('report.json'))['report'] 34 | impls = ['eclair', 'lightning', 'lnd', 'ptarmigan'] 35 | report['versions'] = OrderedDict(sorted({i: get_version(i) for i in impls}.items())) 36 | 37 | # Any unique random id would do really 38 | version_string = "_".join([k + "-" + v for k, v in report['versions'].items()]) 39 | report['id'] = sha256(version_string.encode('ASCII')).hexdigest() 40 | with open(os.path.join('reports', report['id'] + ".json"), "w") as f: 41 | f.write(json.dumps(report)) 42 | 43 | 44 | def group_tests(report): 45 | tests = report['tests'] 46 | report['tests'] = {} 47 | for t in tests: 48 | # Strip filename 49 | splits = t['name'][9:].split('[') 50 | name = splits[0] 51 | config = splits[1][:-1] 52 | t['name'] = config 53 | del t['setup'] 54 | del t['teardown'] 55 | if name not in report['tests']: 56 | report['tests'][name] = {'subtests': [], 'total': 0, 'success': 0} 57 | 58 | report['tests'][name]['subtests'].append(t) 59 | report['tests'][name]['total'] += 1 60 | if t['outcome'] == 'passed': 61 | report['tests'][name]['success'] += 1 62 | report['tests'][name]['subtests'] = sorted(report['tests'][name]['subtests'], key=lambda x: x['name']) 63 | 64 | return report 65 | 66 | def ratio_to_color(ratio): 67 | if ratio > 0.95: 68 | return 'success' 69 | elif ratio > 0.5: 70 | return 'warning' 71 | return 'danger' 72 | 73 | 74 | def load_reports(template): 75 | reports = [] 76 | for fname in os.listdir("reports"): 77 | with open(os.path.join("reports", fname), 'r') as f: 78 | report = json.loads(f.read()) 79 | ratio = report['summary']['passed'] / report['summary']['num_tests'] 80 | report['summary']['color'] = ratio_to_color(ratio) 81 | reports.append(group_tests(report)) 82 | reports = sorted(reports, key=lambda x: x['created_at'])[::-1] 83 | return {'reports': reports} 84 | 85 | 86 | def load_report(template): 87 | with open(template.filename, 'r') as f: 88 | report = json.loads(f.read()) 89 | 90 | return group_tests(report) 91 | 92 | 93 | def render_report(env, template, **report): 94 | report_template = env.get_template("_report.html") 95 | out = "%s/%s.html" % (env.outpath, report['id']) 96 | for k, v in report['tests'].items(): 97 | ratio = v['success'] / v['total'] 98 | report['tests'][k]['color'] = ratio_to_color(ratio) 99 | report_template.stream(**report).dump(out) 100 | 101 | 102 | @click.command() 103 | def html(): 104 | global entries 105 | 106 | s = make_site( 107 | contexts=[ 108 | ('index.html', load_reports), 109 | ('.*.json', load_report), 110 | ], 111 | rules=[ 112 | ('.*.json', render_report), 113 | ], 114 | outpath='output', 115 | staticpaths=('static/',) 116 | ) 117 | s.render() 118 | 119 | 120 | def _get_storage_client(): 121 | return storage.Client(project=os.getenv("GCP_PROJECT")) 122 | 123 | 124 | @click.command() 125 | @click.argument('report_id') 126 | def upload(report_id): 127 | filename = report_id + '.json' 128 | 129 | client = _get_storage_client() 130 | bucket = client.bucket(os.getenv('GCP_STORAGE_BUCKET')) 131 | blob = bucket.blob(filename) 132 | 133 | with open(os.path.join("reports", filename)) as f: 134 | contents = f.read() 135 | 136 | blob.upload_from_string( 137 | contents, 138 | content_type='application/json') 139 | 140 | url = blob.public_url 141 | return url 142 | 143 | 144 | if __name__ == '__main__': 145 | cli.add_command(html) 146 | cli.add_command(postprocess) 147 | cli.add_command(upload) 148 | cli() 149 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # This function is based upon the example of how to 5 | # "[make] test result information available in fixtures" at: 6 | # https://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures 7 | # and: 8 | # https://github.com/pytest-dev/pytest/issues/288 9 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 10 | def pytest_runtest_makereport(item, call): 11 | # execute all other hooks to obtain the report object 12 | outcome = yield 13 | rep = outcome.get_result() 14 | 15 | # set a report attribute for each phase of a call, which can 16 | # be "setup", "call", "teardown" 17 | 18 | setattr(item, "rep_" + rep.when, rep) 19 | -------------------------------------------------------------------------------- /eclair.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | from ephemeral_port_reserve import reserve 3 | from lnaddr import lndecode 4 | from requests.adapters import HTTPAdapter 5 | from requests.packages.urllib3.util.retry import Retry 6 | from utils import TailableProc 7 | 8 | import json 9 | import logging 10 | import os 11 | import psutil 12 | import re 13 | import requests 14 | import shutil 15 | import time 16 | 17 | 18 | def requests_retry_session( 19 | retries=3, 20 | backoff_factor=0.3, 21 | status_forcelist=(500, 502, 504), 22 | session=None, 23 | ): 24 | session = session or requests.Session() 25 | retry = Retry( 26 | total=retries, 27 | read=retries, 28 | connect=retries, 29 | backoff_factor=backoff_factor, 30 | status_forcelist=status_forcelist, 31 | ) 32 | adapter = HTTPAdapter(max_retries=retry) 33 | session.mount('http://', adapter) 34 | session.mount('https://', adapter) 35 | return session 36 | 37 | 38 | class EclairD(TailableProc): 39 | 40 | def __init__(self, lightning_dir, bitcoind, port): 41 | TailableProc.__init__(self, lightning_dir, "eclair({})".format(port)) 42 | self.lightning_dir = lightning_dir 43 | self.bitcoind = bitcoind 44 | self.port = port 45 | self.rpc_port = str(reserve()) 46 | self.prefix = 'eclair' 47 | 48 | self.cmd_line = [ 49 | 'java', 50 | '-Declair.datadir={}'.format(lightning_dir), 51 | '-Dlogback.configurationFile={}'.format(os.path.join(lightning_dir, 'logback.xml')), 52 | '-jar', 53 | 'bin/eclair.jar' 54 | ] 55 | 56 | if not os.path.exists(lightning_dir): 57 | os.makedirs(lightning_dir) 58 | 59 | shutil.copyfile('logback.xml', os.path.join(lightning_dir, "logback.xml")) 60 | 61 | # Adapt the config and store it 62 | with open('src/eclair/eclair-core/src/main/resources/reference.conf') as f: 63 | config = f.read() 64 | 65 | replacements = [ 66 | ('chain = "mainnet"', 'chain = "regtest"'), 67 | ('enabled = false // disabled by default for security reasons', 'enabled = true'), 68 | ('password = ""', 'password = "rpcpass"'), 69 | ('9735', str(port)), 70 | ('8332', str(self.bitcoind.rpcport)), 71 | ('8080', str(self.rpc_port)), 72 | ('"test"', '"regtest"'), 73 | ('"foo"', '"rpcuser"'), 74 | ('"bar"', '"rpcpass"'), 75 | ('zmqblock = "tcp://127.0.0.1:29000"', 'zmqblock = "tcp://127.0.0.1:{}"'.format(self.bitcoind.zmqpubrawblock_port)), 76 | ('zmqtx = "tcp://127.0.0.1:29000"', 'zmqtx = "tcp://127.0.0.1:{}"'.format(self.bitcoind.zmqpubrawtx_port)), 77 | ('use-old-api = false', 'use-old-api = true'), 78 | ] 79 | 80 | for old, new in replacements: 81 | config = config.replace(old, new) 82 | 83 | with open(os.path.join(lightning_dir, "eclair.conf"), "w") as f: 84 | f.write(config) 85 | 86 | def start(self): 87 | TailableProc.start(self) 88 | self.wait_for_log("connected to tcp://127.0.0.1:") 89 | 90 | # And let's also remember the address 91 | exp = 'initial wallet address=([a-zA-Z0-9]+)' 92 | addr_line = self.wait_for_log(exp) 93 | self.addr = re.search(exp, addr_line).group(1) 94 | 95 | self.logger.info("Eclair started (pid: {})".format(self.proc.pid)) 96 | 97 | def stop(self): 98 | # Java forks internally and detaches its children, use psutil to hunt 99 | # them down and kill them 100 | proc = psutil.Process(self.proc.pid) 101 | processes = [proc] + proc.children(recursive=True) 102 | 103 | # Be nice to begin with 104 | for p in processes: 105 | p.terminate() 106 | _, alive = psutil.wait_procs(processes, timeout=3) 107 | 108 | # But if they aren't, we can be more persuasive 109 | for p in alive: 110 | p.kill() 111 | psutil.wait_procs(alive, timeout=3) 112 | self.thread.join() 113 | super().save_log() 114 | 115 | 116 | class EclairNode(object): 117 | 118 | displayName = 'eclair' 119 | 120 | def __init__(self, lightning_dir, lightning_port, btc, executor=None, 121 | node_id=0): 122 | self.bitcoin = btc 123 | self.executor = executor 124 | self.daemon = EclairD(lightning_dir, self.bitcoin, 125 | port=lightning_port) 126 | self.rpc = EclairRpc( 127 | 'http://localhost:{}'.format(self.daemon.rpc_port)) 128 | self.logger = logging.getLogger('eclair-node({})'.format(lightning_port)) 129 | 130 | def peers(self): 131 | return [p['nodeId'] for p in self.rpc.peers()] 132 | 133 | def id(self): 134 | info = self.rpc._call("getinfo", {}) 135 | return info['nodeId'] 136 | 137 | def openchannel(self, node_id, host, port, satoshis): 138 | r = self.rpc._call('open', {"nodeId": node_id, "fundingSatoshis": satoshis, "pushMsat": 0}) 139 | return r 140 | 141 | def getaddress(self): 142 | return self.daemon.addr 143 | 144 | def addfunds(self, bitcoind, satoshis): 145 | addr = self.getaddress() 146 | bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) 147 | 148 | # Eclair seems to grab funds from the block, so give it a 149 | # chance to see it 150 | time.sleep(1) 151 | bitcoind.rpc.generate(1) 152 | 153 | def ping(self): 154 | """ Simple liveness test to see if the node is up and running 155 | 156 | Returns true if the node is reachable via RPC, false otherwise. 157 | """ 158 | try: 159 | self.rpc.help() 160 | return True 161 | except: 162 | return False 163 | 164 | def check_channel(self, remote): 165 | """ Make sure that we have an active channel with remote 166 | """ 167 | self_id = self.id() 168 | remote_id = remote.id() 169 | for c in self.rpc.channels(): 170 | channel = self.rpc.channel(c) 171 | if channel['nodeId'] == remote_id: 172 | self.logger.debug("Channel {} -> {} state: {}".format(self_id, remote_id, channel['state'])) 173 | return channel['state'] == 'NORMAL' 174 | self.logger.warning("Channel {} -> {} not found".format(self_id, remote_id)) 175 | return False 176 | 177 | def getchannels(self): 178 | channels = [] 179 | for c in self.rpc._call('channels', {}): 180 | channels.append((c['a'], c['b'])) 181 | channels.append((c['b'], c['a'])) 182 | return channels 183 | 184 | def getnodes(self): 185 | return set([n['nodeId'] for n in self.rpc.allnodes()]) 186 | 187 | def invoice(self, amount): 188 | req = self.rpc._call("createinvoice", {"amountMsat": amount, "description": "invoice1"}) 189 | logging.debug(req) 190 | return req['serialized'] 191 | 192 | def send(self, req): 193 | details = self.parse_invoice(req) 194 | payment_hash = details['paymentHash'] 195 | payment_id = self.rpc._call("payinvoice", {"invoice": req}) 196 | for i in range(100): 197 | result = self.rpc._call('getsentinfo', {'paymentHash': payment_hash, 'id': payment_id})[0] 198 | if result['status'] == 'SUCCEEDED': 199 | break 200 | time.sleep(1) 201 | if 'failures' in result: 202 | raise ValueError("Failed to send payment: {}".format(result)) 203 | else: 204 | return result['preimage'] 205 | 206 | def parse_invoice(self, invoice): 207 | return self.rpc._call('parseinvoice', {'invoice': invoice}) 208 | 209 | def connect(self, host, port, node_id): 210 | return self.rpc._call('connect', {'nodeId': node_id, 'host': host, 'port': port}) 211 | 212 | def block_sync(self, blockhash): 213 | time.sleep(1) 214 | 215 | def info(self): 216 | r = self.rpc._call('getinfo', {}) 217 | return { 218 | 'id': r['nodeId'], 219 | 'blockheight': r['blockHeight'], 220 | } 221 | 222 | def restart(self): 223 | self.daemon.stop() 224 | time.sleep(5) 225 | self.daemon.start() 226 | time.sleep(1) 227 | 228 | def stop(self): 229 | self.daemon.stop() 230 | 231 | def start(self): 232 | self.daemon.start() 233 | 234 | def check_route(self, node_id, amount): 235 | try: 236 | r = self.rpc._call("findroutetonode", {"nodeId": node_id, "amountMsat": amount}) 237 | except ValueError as e: 238 | if (str(e).find("command failed: route not found") > 0): 239 | return False 240 | raise 241 | return True 242 | 243 | class EclairRpc(object): 244 | 245 | def __init__(self, url): 246 | self.url = url 247 | # self.session = requests_retry_session(retries=10, session=requests.Session()) 248 | 249 | def _call(self, method, params): 250 | #headers = {'Content-type': 'multipart/form-data'} 251 | headers = {} 252 | logging.info("Calling {} with params={}".format(method, json.dumps(params, indent=4, sort_keys=True))) 253 | url = "{}/{}".format(self.url, method) 254 | with requests_retry_session(retries=10, session=requests.Session()) as s: 255 | reply = s.post(url, data=params, headers=headers, auth=('user', 'rpcpass')) 256 | if reply.status_code != 200: 257 | raise ValueError("Server returned an unknown error: {} ({})".format( 258 | reply.status_code, reply.text)) 259 | 260 | logging.debug("Method {} returned {}".format(method, json.dumps(reply.json(), indent=4, sort_keys=True))) 261 | if 'error' in reply.json(): 262 | raise ValueError('Error calling {}: {}'.format( 263 | method, reply.json())) 264 | else: 265 | return reply.json() 266 | 267 | def peers(self): 268 | return self._call('peers', {}) 269 | 270 | def channels(self): 271 | return [c['channelId'] for c in self._call('channels', {})] 272 | 273 | def channel(self, cid): 274 | return self._call('channel', {'channelId': cid}) 275 | 276 | def allnodes(self): 277 | return self._call('allnodes', {}) 278 | 279 | def help(self): 280 | return self._call('getinfo', {}) 281 | -------------------------------------------------------------------------------- /fixtures.py: -------------------------------------------------------------------------------- 1 | from btcproxy import ProxiedBitcoinD 2 | from ephemeral_port_reserve import reserve 3 | from concurrent import futures 4 | 5 | import os 6 | import pytest 7 | import tempfile 8 | import logging 9 | import shutil 10 | 11 | 12 | TEST_DIR = tempfile.mkdtemp(prefix='lightning-') 13 | TEST_DEBUG = os.getenv("TEST_DEBUG", "0") == "1" 14 | 15 | 16 | # A dict in which we count how often a particular test has run so far. Used to 17 | # give each attempt its own numbered directory, and avoid clashes. 18 | __attempts = {} 19 | 20 | 21 | class NodeFactory(object): 22 | """A factory to setup and start `lightningd` daemons. 23 | """ 24 | def __init__(self, testname, executor, bitcoind, btcd): 25 | self.testname = testname 26 | self.next_id = 1 27 | self.nodes = [] 28 | self.executor = executor 29 | self.bitcoind = bitcoind 30 | self.btcd = btcd 31 | 32 | def get_node(self, implementation): 33 | node_id = self.next_id 34 | self.next_id += 1 35 | 36 | lightning_dir = os.path.join( 37 | TEST_DIR, self.testname, "node-{}/".format(node_id)) 38 | port = reserve() 39 | 40 | node = implementation(lightning_dir, port, self.bitcoind, 41 | executor=self.executor, node_id=node_id) 42 | self.nodes.append(node) 43 | 44 | node.btcd = self.btcd 45 | node.daemon.start() 46 | return node 47 | 48 | def killall(self): 49 | for n in self.nodes: 50 | n.daemon.stop() 51 | 52 | 53 | @pytest.fixture 54 | def directory(request, test_base_dir, test_name): 55 | """Return a per-test specific directory. 56 | 57 | This makes a unique test-directory even if a test is rerun multiple times. 58 | 59 | """ 60 | global __attempts 61 | # Auto set value if it isn't in the dict yet 62 | __attempts[test_name] = __attempts.get(test_name, 0) + 1 63 | directory = os.path.join(test_base_dir, "{}_{}".format(test_name, __attempts[test_name])) 64 | request.node.has_errors = False 65 | 66 | yield directory 67 | 68 | # This uses the status set in conftest.pytest_runtest_makereport to 69 | # determine whether we succeeded or failed. 70 | if not request.node.has_errors and request.node.rep_call.outcome == 'passed': 71 | shutil.rmtree(directory) 72 | else: 73 | logging.debug("Test execution failed, leaving the test directory {} intact.".format(directory)) 74 | 75 | 76 | @pytest.fixture(scope="session") 77 | def test_base_dir(): 78 | directory = tempfile.mkdtemp(prefix='ltests-') 79 | print("Running tests in {}".format(directory)) 80 | 81 | yield directory 82 | 83 | if os.listdir(directory) == []: 84 | shutil.rmtree(directory) 85 | 86 | 87 | @pytest.fixture 88 | def test_name(request): 89 | yield request.function.__name__ 90 | 91 | 92 | @pytest.fixture() 93 | def bitcoind(directory): 94 | proxyport = reserve() 95 | btc = ProxiedBitcoinD(bitcoin_dir=os.path.join(directory, "bitcoind"), proxyport=proxyport) 96 | btc.start() 97 | bch_info = btc.rpc.getblockchaininfo() 98 | w_info = btc.rpc.getwalletinfo() 99 | addr = btc.rpc.getnewaddress() 100 | # Make sure we have segwit and some funds 101 | if bch_info['blocks'] < 120: 102 | logging.debug("SegWit not active, generating some more blocks") 103 | btc.rpc.generatetoaddress(120 - bch_info['blocks'], addr) 104 | elif w_info['balance'] < 1: 105 | logging.debug("Insufficient balance, generating 1 block") 106 | btc.rpc.generatetoaddress(1, addr) 107 | 108 | # Mock `estimatesmartfee` to make c-lightning happy 109 | def mock_estimatesmartfee(r): 110 | return {"id": r['id'], "error": None, "result": {"feerate": 0.00100001, "blocks": r['params'][0]}} 111 | 112 | btc.mock_rpc('estimatesmartfee', mock_estimatesmartfee) 113 | 114 | yield btc 115 | 116 | try: 117 | btc.rpc.stop() 118 | except Exception: 119 | btc.proc.kill() 120 | btc.proc.wait() 121 | 122 | 123 | @pytest.fixture(scope="module") 124 | def btcd(): 125 | btcd = BtcD() 126 | btcd.start() 127 | 128 | yield btcd 129 | 130 | try: 131 | btcd.rpc.stop() 132 | except: 133 | btcd.proc.kill() 134 | btcd.proc.wait() 135 | 136 | 137 | @pytest.fixture 138 | def node_factory(request, bitcoind): 139 | executor = futures.ThreadPoolExecutor(max_workers=20) 140 | node_factory = NodeFactory(request._pyfuncitem.name, executor, bitcoind, None) 141 | yield node_factory 142 | node_factory.killall() 143 | executor.shutdown(wait=False) 144 | 145 | 146 | -------------------------------------------------------------------------------- /google/api/annotations_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: google/api/annotations.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | from google.api import http_pb2 as google_dot_api_dot_http__pb2 17 | from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 18 | 19 | 20 | DESCRIPTOR = _descriptor.FileDescriptor( 21 | name='google/api/annotations.proto', 22 | package='google.api', 23 | syntax='proto3', 24 | serialized_pb=_b('\n\x1cgoogle/api/annotations.proto\x12\ngoogle.api\x1a\x15google/api/http.proto\x1a google/protobuf/descriptor.proto:E\n\x04http\x12\x1e.google.protobuf.MethodOptions\x18\xb0\xca\xbc\" \x01(\x0b\x32\x14.google.api.HttpRuleBn\n\x0e\x63om.google.apiB\x10\x41nnotationsProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3') 25 | , 26 | dependencies=[google_dot_api_dot_http__pb2.DESCRIPTOR,google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) 27 | 28 | 29 | HTTP_FIELD_NUMBER = 72295728 30 | http = _descriptor.FieldDescriptor( 31 | name='http', full_name='google.api.http', index=0, 32 | number=72295728, type=11, cpp_type=10, label=1, 33 | has_default_value=False, default_value=None, 34 | message_type=None, enum_type=None, containing_type=None, 35 | is_extension=True, extension_scope=None, 36 | options=None) 37 | 38 | DESCRIPTOR.extensions_by_name['http'] = http 39 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 40 | 41 | http.message_type = google_dot_api_dot_http__pb2._HTTPRULE 42 | google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(http) 43 | 44 | DESCRIPTOR.has_options = True 45 | DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\016com.google.apiB\020AnnotationsProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI')) 46 | try: 47 | # THESE ELEMENTS WILL BE DEPRECATED. 48 | # Please use the generated *_pb2_grpc.py files instead. 49 | import grpc 50 | from grpc.beta import implementations as beta_implementations 51 | from grpc.beta import interfaces as beta_interfaces 52 | from grpc.framework.common import cardinality 53 | from grpc.framework.interfaces.face import utilities as face_utilities 54 | except ImportError: 55 | pass 56 | # @@protoc_insertion_point(module_scope) 57 | -------------------------------------------------------------------------------- /google/api/annotations_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | -------------------------------------------------------------------------------- /google/api/http_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: google/api/http.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='google/api/http.proto', 20 | package='google.api', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x15google/api/http.proto\x12\ngoogle.api\"+\n\x04Http\x12#\n\x05rules\x18\x01 \x03(\x0b\x32\x14.google.api.HttpRule\"\xea\x01\n\x08HttpRule\x12\x10\n\x08selector\x18\x01 \x01(\t\x12\r\n\x03get\x18\x02 \x01(\tH\x00\x12\r\n\x03put\x18\x03 \x01(\tH\x00\x12\x0e\n\x04post\x18\x04 \x01(\tH\x00\x12\x10\n\x06\x64\x65lete\x18\x05 \x01(\tH\x00\x12\x0f\n\x05patch\x18\x06 \x01(\tH\x00\x12/\n\x06\x63ustom\x18\x08 \x01(\x0b\x32\x1d.google.api.CustomHttpPatternH\x00\x12\x0c\n\x04\x62ody\x18\x07 \x01(\t\x12\x31\n\x13\x61\x64\x64itional_bindings\x18\x0b \x03(\x0b\x32\x14.google.api.HttpRuleB\t\n\x07pattern\"/\n\x11\x43ustomHttpPattern\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\tBj\n\x0e\x63om.google.apiB\tHttpProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xf8\x01\x01\xa2\x02\x04GAPIb\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _HTTP = _descriptor.Descriptor( 29 | name='Http', 30 | full_name='google.api.Http', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='rules', full_name='google.api.Http.rules', index=0, 37 | number=1, type=11, cpp_type=10, label=3, 38 | has_default_value=False, default_value=[], 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | options=None), 42 | ], 43 | extensions=[ 44 | ], 45 | nested_types=[], 46 | enum_types=[ 47 | ], 48 | options=None, 49 | is_extendable=False, 50 | syntax='proto3', 51 | extension_ranges=[], 52 | oneofs=[ 53 | ], 54 | serialized_start=37, 55 | serialized_end=80, 56 | ) 57 | 58 | 59 | _HTTPRULE = _descriptor.Descriptor( 60 | name='HttpRule', 61 | full_name='google.api.HttpRule', 62 | filename=None, 63 | file=DESCRIPTOR, 64 | containing_type=None, 65 | fields=[ 66 | _descriptor.FieldDescriptor( 67 | name='selector', full_name='google.api.HttpRule.selector', index=0, 68 | number=1, type=9, cpp_type=9, label=1, 69 | has_default_value=False, default_value=_b("").decode('utf-8'), 70 | message_type=None, enum_type=None, containing_type=None, 71 | is_extension=False, extension_scope=None, 72 | options=None), 73 | _descriptor.FieldDescriptor( 74 | name='get', full_name='google.api.HttpRule.get', index=1, 75 | number=2, type=9, cpp_type=9, label=1, 76 | has_default_value=False, default_value=_b("").decode('utf-8'), 77 | message_type=None, enum_type=None, containing_type=None, 78 | is_extension=False, extension_scope=None, 79 | options=None), 80 | _descriptor.FieldDescriptor( 81 | name='put', full_name='google.api.HttpRule.put', index=2, 82 | number=3, type=9, cpp_type=9, label=1, 83 | has_default_value=False, default_value=_b("").decode('utf-8'), 84 | message_type=None, enum_type=None, containing_type=None, 85 | is_extension=False, extension_scope=None, 86 | options=None), 87 | _descriptor.FieldDescriptor( 88 | name='post', full_name='google.api.HttpRule.post', index=3, 89 | number=4, type=9, cpp_type=9, label=1, 90 | has_default_value=False, default_value=_b("").decode('utf-8'), 91 | message_type=None, enum_type=None, containing_type=None, 92 | is_extension=False, extension_scope=None, 93 | options=None), 94 | _descriptor.FieldDescriptor( 95 | name='delete', full_name='google.api.HttpRule.delete', index=4, 96 | number=5, type=9, cpp_type=9, label=1, 97 | has_default_value=False, default_value=_b("").decode('utf-8'), 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | options=None), 101 | _descriptor.FieldDescriptor( 102 | name='patch', full_name='google.api.HttpRule.patch', index=5, 103 | number=6, type=9, cpp_type=9, label=1, 104 | has_default_value=False, default_value=_b("").decode('utf-8'), 105 | message_type=None, enum_type=None, containing_type=None, 106 | is_extension=False, extension_scope=None, 107 | options=None), 108 | _descriptor.FieldDescriptor( 109 | name='custom', full_name='google.api.HttpRule.custom', index=6, 110 | number=8, type=11, cpp_type=10, label=1, 111 | has_default_value=False, default_value=None, 112 | message_type=None, enum_type=None, containing_type=None, 113 | is_extension=False, extension_scope=None, 114 | options=None), 115 | _descriptor.FieldDescriptor( 116 | name='body', full_name='google.api.HttpRule.body', index=7, 117 | number=7, type=9, cpp_type=9, label=1, 118 | has_default_value=False, default_value=_b("").decode('utf-8'), 119 | message_type=None, enum_type=None, containing_type=None, 120 | is_extension=False, extension_scope=None, 121 | options=None), 122 | _descriptor.FieldDescriptor( 123 | name='additional_bindings', full_name='google.api.HttpRule.additional_bindings', index=8, 124 | number=11, type=11, cpp_type=10, label=3, 125 | has_default_value=False, default_value=[], 126 | message_type=None, enum_type=None, containing_type=None, 127 | is_extension=False, extension_scope=None, 128 | options=None), 129 | ], 130 | extensions=[ 131 | ], 132 | nested_types=[], 133 | enum_types=[ 134 | ], 135 | options=None, 136 | is_extendable=False, 137 | syntax='proto3', 138 | extension_ranges=[], 139 | oneofs=[ 140 | _descriptor.OneofDescriptor( 141 | name='pattern', full_name='google.api.HttpRule.pattern', 142 | index=0, containing_type=None, fields=[]), 143 | ], 144 | serialized_start=83, 145 | serialized_end=317, 146 | ) 147 | 148 | 149 | _CUSTOMHTTPPATTERN = _descriptor.Descriptor( 150 | name='CustomHttpPattern', 151 | full_name='google.api.CustomHttpPattern', 152 | filename=None, 153 | file=DESCRIPTOR, 154 | containing_type=None, 155 | fields=[ 156 | _descriptor.FieldDescriptor( 157 | name='kind', full_name='google.api.CustomHttpPattern.kind', index=0, 158 | number=1, type=9, cpp_type=9, label=1, 159 | has_default_value=False, default_value=_b("").decode('utf-8'), 160 | message_type=None, enum_type=None, containing_type=None, 161 | is_extension=False, extension_scope=None, 162 | options=None), 163 | _descriptor.FieldDescriptor( 164 | name='path', full_name='google.api.CustomHttpPattern.path', index=1, 165 | number=2, type=9, cpp_type=9, label=1, 166 | has_default_value=False, default_value=_b("").decode('utf-8'), 167 | message_type=None, enum_type=None, containing_type=None, 168 | is_extension=False, extension_scope=None, 169 | options=None), 170 | ], 171 | extensions=[ 172 | ], 173 | nested_types=[], 174 | enum_types=[ 175 | ], 176 | options=None, 177 | is_extendable=False, 178 | syntax='proto3', 179 | extension_ranges=[], 180 | oneofs=[ 181 | ], 182 | serialized_start=319, 183 | serialized_end=366, 184 | ) 185 | 186 | _HTTP.fields_by_name['rules'].message_type = _HTTPRULE 187 | _HTTPRULE.fields_by_name['custom'].message_type = _CUSTOMHTTPPATTERN 188 | _HTTPRULE.fields_by_name['additional_bindings'].message_type = _HTTPRULE 189 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 190 | _HTTPRULE.fields_by_name['get']) 191 | _HTTPRULE.fields_by_name['get'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 192 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 193 | _HTTPRULE.fields_by_name['put']) 194 | _HTTPRULE.fields_by_name['put'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 195 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 196 | _HTTPRULE.fields_by_name['post']) 197 | _HTTPRULE.fields_by_name['post'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 198 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 199 | _HTTPRULE.fields_by_name['delete']) 200 | _HTTPRULE.fields_by_name['delete'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 201 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 202 | _HTTPRULE.fields_by_name['patch']) 203 | _HTTPRULE.fields_by_name['patch'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 204 | _HTTPRULE.oneofs_by_name['pattern'].fields.append( 205 | _HTTPRULE.fields_by_name['custom']) 206 | _HTTPRULE.fields_by_name['custom'].containing_oneof = _HTTPRULE.oneofs_by_name['pattern'] 207 | DESCRIPTOR.message_types_by_name['Http'] = _HTTP 208 | DESCRIPTOR.message_types_by_name['HttpRule'] = _HTTPRULE 209 | DESCRIPTOR.message_types_by_name['CustomHttpPattern'] = _CUSTOMHTTPPATTERN 210 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 211 | 212 | Http = _reflection.GeneratedProtocolMessageType('Http', (_message.Message,), dict( 213 | DESCRIPTOR = _HTTP, 214 | __module__ = 'google.api.http_pb2' 215 | # @@protoc_insertion_point(class_scope:google.api.Http) 216 | )) 217 | _sym_db.RegisterMessage(Http) 218 | 219 | HttpRule = _reflection.GeneratedProtocolMessageType('HttpRule', (_message.Message,), dict( 220 | DESCRIPTOR = _HTTPRULE, 221 | __module__ = 'google.api.http_pb2' 222 | # @@protoc_insertion_point(class_scope:google.api.HttpRule) 223 | )) 224 | _sym_db.RegisterMessage(HttpRule) 225 | 226 | CustomHttpPattern = _reflection.GeneratedProtocolMessageType('CustomHttpPattern', (_message.Message,), dict( 227 | DESCRIPTOR = _CUSTOMHTTPPATTERN, 228 | __module__ = 'google.api.http_pb2' 229 | # @@protoc_insertion_point(class_scope:google.api.CustomHttpPattern) 230 | )) 231 | _sym_db.RegisterMessage(CustomHttpPattern) 232 | 233 | 234 | DESCRIPTOR.has_options = True 235 | DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\016com.google.apiB\tHttpProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\370\001\001\242\002\004GAPI')) 236 | try: 237 | # THESE ELEMENTS WILL BE DEPRECATED. 238 | # Please use the generated *_pb2_grpc.py files instead. 239 | import grpc 240 | from grpc.beta import implementations as beta_implementations 241 | from grpc.beta import interfaces as beta_interfaces 242 | from grpc.framework.common import cardinality 243 | from grpc.framework.interfaces.face import utilities as face_utilities 244 | except ImportError: 245 | pass 246 | # @@protoc_insertion_point(module_scope) 247 | -------------------------------------------------------------------------------- /google/api/http_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | -------------------------------------------------------------------------------- /lightningd.py: -------------------------------------------------------------------------------- 1 | from lightning import LightningRpc 2 | from utils import TailableProc 3 | 4 | import json 5 | import logging 6 | import os 7 | import time 8 | 9 | 10 | LIGHTNINGD_CONFIG = { 11 | "log-level": "debug", 12 | "watchtime-blocks": 6, 13 | "network": "regtest", 14 | } 15 | 16 | 17 | class LightningD(TailableProc): 18 | 19 | def __init__(self, lightning_dir, bitcoind, port=9735): 20 | TailableProc.__init__(self, lightning_dir) 21 | self.lightning_dir = lightning_dir 22 | self.port = port 23 | self.bitcoind = bitcoind 24 | self.cmd_line = [ 25 | 'bin/lightningd', 26 | '--bitcoin-datadir={}'.format(self.bitcoind.bitcoin_dir), 27 | '--lightning-dir={}'.format(lightning_dir), 28 | '--addr=127.0.0.1:{}'.format(port), 29 | '--dev-bitcoind-poll=1', 30 | '--plugin-dir=src/lightning/plugins', 31 | 32 | # The following are temporary workarounds 33 | '--bitcoin-rpcport={}'.format(self.bitcoind.rpcport), 34 | '--bitcoin-rpcuser=rpcuser', 35 | '--bitcoin-rpcpassword=rpcpass', 36 | ] 37 | self.cmd_line += [ 38 | "--{}={}".format(k, v) for k, v in LIGHTNINGD_CONFIG.items() 39 | ] 40 | self.prefix = 'lightningd' 41 | 42 | if not os.path.exists(lightning_dir): 43 | os.makedirs(lightning_dir) 44 | 45 | def start(self): 46 | TailableProc.start(self) 47 | self.wait_for_log("Server started with public key") 48 | time.sleep(5) 49 | logging.info("LightningD started") 50 | 51 | def stop(self): 52 | TailableProc.stop(self) 53 | logging.info("LightningD stopped") 54 | 55 | 56 | class LightningNode(object): 57 | 58 | displayName = 'lightning' 59 | 60 | def __init__(self, lightning_dir, lightning_port, btc, executor=None, node_id=0): 61 | self.bitcoin = btc 62 | self.executor = executor 63 | self.daemon = LightningD(lightning_dir, self.bitcoin, 64 | port=lightning_port) 65 | socket_path = os.path.join(lightning_dir, "lightning-rpc").format( 66 | node_id) 67 | self.invoice_count = 0 68 | self.logger = logging.getLogger('lightning-node({})'.format(lightning_port)) 69 | 70 | self.rpc = LightningRpc(socket_path, self.executor) 71 | 72 | orig_call = self.rpc._call 73 | 74 | def rpc_call(method, args): 75 | self.logger.debug("Calling {} with arguments {}".format(method, json.dumps(args, indent=4, sort_keys=True))) 76 | r = orig_call(method, args) 77 | self.logger.debug("Call returned {}".format(json.dumps(r, indent=4, sort_keys=True))) 78 | return r 79 | 80 | self.rpc._call = rpc_call 81 | self.myid = None 82 | 83 | def peers(self): 84 | return [p['id'] for p in self.rpc.listpeers()['peers']] 85 | 86 | def getinfo(self): 87 | if not self.info: 88 | self.info = self.rpc.getinfo() 89 | return self.info 90 | 91 | def id(self): 92 | if not self.myid: 93 | self.myid = self.rpc.getinfo()['id'] 94 | return self.myid 95 | 96 | def openchannel(self, node_id, host, port, satoshis): 97 | # Make sure we have a connection already 98 | if node_id not in self.peers(): 99 | raise ValueError("Must connect to node before opening a channel") 100 | return self.rpc.fundchannel(node_id, satoshis) 101 | 102 | def getaddress(self): 103 | return self.rpc.newaddr()['address'] 104 | 105 | def addfunds(self, bitcoind, satoshis): 106 | addr = self.getaddress() 107 | btc_addr = bitcoind.rpc.getnewaddress() 108 | txid = bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) 109 | bitcoind.rpc.getrawtransaction(txid) 110 | while len(self.rpc.listfunds()['outputs']) == 0: 111 | time.sleep(1) 112 | bitcoind.rpc.generatetoaddress(1, btc_addr) 113 | 114 | def ping(self): 115 | """ Simple liveness test to see if the node is up and running 116 | 117 | Returns true if the node is reachable via RPC, false otherwise. 118 | """ 119 | try: 120 | self.rpc.help() 121 | return True 122 | except: 123 | return False 124 | 125 | def check_channel(self, remote, require_both=False): 126 | """Make sure that we have an active channel with remote 127 | 128 | `require_both` must be False unless the other side supports 129 | sending a `channel_announcement` and `channel_update` on 130 | `funding_locked`. This doesn't work for eclair for example. 131 | 132 | """ 133 | remote_id = remote.id() 134 | self_id = self.id() 135 | peer = None 136 | for p in self.rpc.listpeers()['peers']: 137 | if remote.id() == p['id']: 138 | peer = p 139 | break 140 | if not peer: 141 | self.logger.debug('Peer {} not found in listpeers'.format(remote)) 142 | return False 143 | 144 | if len(peer['channels']) < 1: 145 | self.logger.debug('Peer {} has no channel open with us'.format(remote)) 146 | return False 147 | 148 | state = p['channels'][0]['state'] 149 | self.logger.debug("Channel {} -> {} state: {}".format(self_id, remote_id, state)) 150 | 151 | if state != 'CHANNELD_NORMAL' or not p['connected']: 152 | self.logger.debug('Channel with peer {} is not in state normal ({}) or peer is not connected ({})'.format( 153 | remote_id, state, p['connected'])) 154 | return False 155 | 156 | # Make sure that gossipd sees a local channel_update for routing 157 | scid = p['channels'][0]['short_channel_id'] 158 | 159 | channels = self.rpc.listchannels(scid)['channels'] 160 | 161 | if not require_both and len(channels) >= 1: 162 | return channels[0]['active'] 163 | 164 | if len(channels) != 2: 165 | self.logger.debug('Waiting for both channel directions to be announced: 2 != {}'.format(len(channels))) 166 | return False 167 | 168 | return channels[0]['active'] and channels[1]['active'] 169 | 170 | def getchannels(self): 171 | result = [] 172 | for c in self.rpc.listchannels()['channels']: 173 | result.append((c['source'], c['destination'])) 174 | return set(result) 175 | 176 | def getnodes(self): 177 | return set([n['nodeid'] for n in self.rpc.listnodes()['nodes']]) 178 | 179 | def invoice(self, amount): 180 | invoice = self.rpc.invoice(amount, "invoice%d" % (self.invoice_count), "description") 181 | self.invoice_count += 1 182 | return invoice['bolt11'] 183 | 184 | def send(self, req): 185 | result = self.rpc.pay(req) 186 | return result['payment_preimage'] 187 | 188 | def connect(self, host, port, node_id): 189 | return self.rpc.connect(node_id, host, port) 190 | 191 | def info(self): 192 | r = self.rpc.getinfo() 193 | return { 194 | 'id': r['id'], 195 | 'blockheight': r['blockheight'], 196 | } 197 | 198 | def block_sync(self, blockhash): 199 | time.sleep(1) 200 | 201 | def restart(self): 202 | self.daemon.stop() 203 | time.sleep(5) 204 | self.daemon.start() 205 | time.sleep(1) 206 | 207 | def stop(self): 208 | self.daemon.stop() 209 | 210 | def start(self): 211 | self.daemon.start() 212 | 213 | def check_route(self, node_id, amount): 214 | try: 215 | r = self.rpc.getroute(node_id, amount, 1.0) 216 | except ValueError as e: 217 | if (str(e).find("Could not find a route") > 0): 218 | return False 219 | raise 220 | return True 221 | -------------------------------------------------------------------------------- /lnaddr.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | from bech32 import bech32_encode, bech32_decode, CHARSET 3 | from binascii import hexlify, unhexlify 4 | from bitstring import BitArray 5 | from decimal import Decimal 6 | 7 | import base58 8 | import bitstring 9 | import hashlib 10 | import math 11 | import re 12 | import secp256k1 13 | import sys 14 | import time 15 | 16 | 17 | # BOLT #11: 18 | # 19 | # A writer MUST encode `amount` as a positive decimal integer with no 20 | # leading zeroes, SHOULD use the shortest representation possible. 21 | def shorten_amount(amount): 22 | """ Given an amount in bitcoin, shorten it 23 | """ 24 | # Convert to pico initially 25 | amount = int(amount * 10**12) 26 | units = ['p', 'n', 'u', 'm', ''] 27 | for unit in units: 28 | if amount % 1000 == 0: 29 | amount //= 1000 30 | else: 31 | break 32 | return str(amount) + unit 33 | 34 | def unshorten_amount(amount): 35 | """ Given a shortened amount, convert it into a decimal 36 | """ 37 | # BOLT #11: 38 | # The following `multiplier` letters are defined: 39 | # 40 | #* `m` (milli): multiply by 0.001 41 | #* `u` (micro): multiply by 0.000001 42 | #* `n` (nano): multiply by 0.000000001 43 | #* `p` (pico): multiply by 0.000000000001 44 | units = { 45 | 'p': 10**12, 46 | 'n': 10**9, 47 | 'u': 10**6, 48 | 'm': 10**3, 49 | } 50 | unit = str(amount)[-1] 51 | # BOLT #11: 52 | # A reader SHOULD fail if `amount` contains a non-digit, or is followed by 53 | # anything except a `multiplier` in the table above. 54 | if not re.fullmatch("\d+[pnum]?", str(amount)): 55 | raise ValueError("Invalid amount '{}'".format(amount)) 56 | 57 | if unit in units.keys(): 58 | return Decimal(amount[:-1]) / units[unit] 59 | else: 60 | return Decimal(amount) 61 | 62 | # Bech32 spits out array of 5-bit values. Shim here. 63 | def u5_to_bitarray(arr): 64 | ret = bitstring.BitArray() 65 | for a in arr: 66 | ret += bitstring.pack("uint:5", a) 67 | return ret 68 | 69 | def bitarray_to_u5(barr): 70 | assert barr.len % 5 == 0 71 | ret = [] 72 | s = bitstring.ConstBitStream(barr) 73 | while s.pos != s.len: 74 | ret.append(s.read(5).uint) 75 | return ret 76 | 77 | def encode_fallback(fallback, currency): 78 | """ Encode all supported fallback addresses. 79 | """ 80 | if currency == 'bc' or currency == 'tb': 81 | fbhrp, witness = bech32_decode(fallback) 82 | if fbhrp: 83 | if fbhrp != currency: 84 | raise ValueError("Not a bech32 address for this currency") 85 | wver = witness[0] 86 | if wver > 16: 87 | raise ValueError("Invalid witness version {}".format(witness[0])) 88 | wprog = u5_to_bitarray(witness[1:]) 89 | else: 90 | addr = base58.b58decode_check(fallback) 91 | if is_p2pkh(currency, addr[0]): 92 | wver = 17 93 | elif is_p2sh(currency, addr[0]): 94 | wver = 18 95 | else: 96 | raise ValueError("Unknown address type for {}".format(currency)) 97 | wprog = addr[1:] 98 | return tagged('f', bitstring.pack("uint:5", wver) + wprog) 99 | else: 100 | raise NotImplementedError("Support for currency {} not implemented".format(currency)) 101 | 102 | def parse_fallback(fallback, currency): 103 | if currency == 'bc' or currency == 'tb': 104 | wver = fallback[0:5].uint 105 | if wver == 17: 106 | addr=base58.b58encode_check(bytes([base58_prefix_map[currency][0]]) 107 | + fallback[5:].tobytes()) 108 | elif wver == 18: 109 | addr=base58.b58encode_check(bytes([base58_prefix_map[currency][1]]) 110 | + fallback[5:].tobytes()) 111 | elif wver <= 16: 112 | addr=bech32_encode(currency, bitarray_to_u5(fallback)) 113 | else: 114 | return None 115 | else: 116 | addr=fallback.tobytes() 117 | return addr 118 | 119 | 120 | # Map of classical and witness address prefixes 121 | base58_prefix_map = { 122 | 'bc' : (0, 5), 123 | 'tb' : (111, 196) 124 | } 125 | 126 | def is_p2pkh(currency, prefix): 127 | return prefix == base58_prefix_map[currency][0] 128 | 129 | def is_p2sh(currency, prefix): 130 | return prefix == base58_prefix_map[currency][1] 131 | 132 | # Tagged field containing BitArray 133 | def tagged(char, l): 134 | # Tagged fields need to be zero-padded to 5 bits. 135 | while l.len % 5 != 0: 136 | l.append('0b0') 137 | return bitstring.pack("uint:5, uint:5, uint:5", 138 | CHARSET.find(char), 139 | (l.len / 5) / 32, (l.len / 5) % 32) + l 140 | 141 | # Tagged field containing bytes 142 | def tagged_bytes(char, l): 143 | return tagged(char, bitstring.BitArray(l)) 144 | 145 | # Discard trailing bits, convert to bytes. 146 | def trim_to_bytes(barr): 147 | # Adds a byte if necessary. 148 | b = barr.tobytes() 149 | if barr.len % 8 != 0: 150 | return b[:-1] 151 | return b 152 | 153 | # Try to pull out tagged data: returns tag, tagged data and remainder. 154 | def pull_tagged(stream): 155 | tag = stream.read(5).uint 156 | length = stream.read(5).uint * 32 + stream.read(5).uint 157 | return (CHARSET[tag], stream.read(length * 5), stream) 158 | 159 | def lnencode(addr, privkey): 160 | if addr.amount: 161 | amount = Decimal(str(addr.amount)) 162 | # We can only send down to millisatoshi. 163 | if amount * 10**12 % 10: 164 | raise ValueError("Cannot encode {}: too many decimal places".format( 165 | addr.amount)) 166 | 167 | amount = addr.currency + shorten_amount(amount) 168 | else: 169 | amount = addr.currency if addr.currency else '' 170 | 171 | hrp = 'ln' + amount 172 | 173 | # Start with the timestamp 174 | data = bitstring.pack('uint:35', addr.date) 175 | 176 | # Payment hash 177 | data += tagged_bytes('p', addr.paymenthash) 178 | tags_set = set() 179 | 180 | for k, v in addr.tags: 181 | 182 | # BOLT #11: 183 | # 184 | # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, 185 | if k in ('d', 'h', 'n', 'x'): 186 | if k in tags_set: 187 | raise ValueError("Duplicate '{}' tag".format(k)) 188 | 189 | if k == 'r': 190 | pubkey, channel, fee, cltv = v 191 | route = bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:64', fee) + bitstring.pack('intbe:16', cltv) 192 | data += tagged('r', route) 193 | elif k == 'f': 194 | data += encode_fallback(v, addr.currency) 195 | elif k == 'd': 196 | data += tagged_bytes('d', v.encode()) 197 | elif k == 'x': 198 | # Get minimal length by trimming leading 5 bits at a time. 199 | expirybits = bitstring.pack('intbe:64', v)[4:64] 200 | while expirybits.startswith('0b00000'): 201 | expirybits = expirybits[5:] 202 | data += tagged('x', expirybits) 203 | elif k == 'h': 204 | data += tagged_bytes('h', hashlib.sha256(v.encode('utf-8')).digest()) 205 | elif k == 'n': 206 | data += tagged_bytes('n', v) 207 | else: 208 | # FIXME: Support unknown tags? 209 | raise ValueError("Unknown tag {}".format(k)) 210 | 211 | tags_set.add(k) 212 | 213 | # BOLT #11: 214 | # 215 | # A writer MUST include either a `d` or `h` field, and MUST NOT include 216 | # both. 217 | if 'd' in tags_set and 'h' in tags_set: 218 | raise ValueError("Cannot include both 'd' and 'h'") 219 | if not 'd' in tags_set and not 'h' in tags_set: 220 | raise ValueError("Must include either 'd' or 'h'") 221 | 222 | # We actually sign the hrp, then data (padded to 8 bits with zeroes). 223 | privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey))) 224 | sig = privkey.ecdsa_sign_recoverable(bytearray([ord(c) for c in hrp]) + data.tobytes()) 225 | # This doesn't actually serialize, but returns a pair of values :( 226 | sig, recid = privkey.ecdsa_recoverable_serialize(sig) 227 | data += bytes(sig) + bytes([recid]) 228 | 229 | return bech32_encode(hrp, bitarray_to_u5(data)) 230 | 231 | class LnAddr(object): 232 | def __init__(self, paymenthash=None, amount=None, currency='bc', tags=None, date=None): 233 | self.date = int(time.time()) if not date else int(date) 234 | self.tags = [] if not tags else tags 235 | self.unknown_tags = [] 236 | self.paymenthash = paymenthash 237 | self.signature = None 238 | self.pubkey = None 239 | self.currency = currency 240 | self.amount = amount 241 | self.min_final_cltv_expiry = None 242 | 243 | def __str__(self): 244 | return "LnAddr[{}, amount={}{} tags=[{}]]".format( 245 | hexlify(self.pubkey.serialize()).decode('utf-8'), 246 | self.amount, self.currency, 247 | ", ".join([k + '=' + str(v) for k, v in self.tags]) 248 | ) 249 | 250 | def lndecode(a): 251 | hrp, data = bech32_decode(a) 252 | if not hrp: 253 | raise ValueError("Bad bech32 checksum") 254 | 255 | # BOLT #11: 256 | # 257 | # A reader MUST fail if it does not understand the `prefix`. 258 | if not hrp.startswith('ln'): 259 | raise ValueError("Does not start with ln") 260 | 261 | data = u5_to_bitarray(data); 262 | 263 | # Final signature 65 bytes, split it off. 264 | if len(data) < 65*8: 265 | raise ValueError("Too short to contain signature") 266 | sigdecoded = data[-65*8:].tobytes() 267 | data = bitstring.ConstBitStream(data[:-65*8]) 268 | 269 | addr = LnAddr() 270 | addr.pubkey = None 271 | 272 | m = re.search("[^\d]+", hrp[2:]) 273 | if m: 274 | addr.currency = m.group(0) 275 | amountstr = hrp[2+m.end():] 276 | # BOLT #11: 277 | # 278 | # A reader SHOULD indicate if amount is unspecified, otherwise it MUST 279 | # multiply `amount` by the `multiplier` value (if any) to derive the 280 | # amount required for payment. 281 | if amountstr != '': 282 | addr.amount = unshorten_amount(amountstr) 283 | 284 | addr.date = data.read(35).uint 285 | 286 | while data.pos != data.len: 287 | tag, tagdata, data = pull_tagged(data) 288 | 289 | # BOLT #11: 290 | # 291 | # A reader MUST skip over unknown fields, an `f` field with unknown 292 | # `version`, or a `p`, `h`, `n` or `r` field which does not have 293 | # `data_length` 52, 52, 53 or 82 respectively. 294 | data_length = len(tagdata) / 5 295 | 296 | if tag == 'r': 297 | if data_length != 82: 298 | addr.unknown_tags.append((tag, tagdata)) 299 | continue 300 | 301 | tagbytes = trim_to_bytes(tagdata) 302 | 303 | addr.tags.append(('r',( 304 | tagbytes[0:33], 305 | tagbytes[33:41], 306 | tagdata[41*8:49*8].intbe, 307 | tagdata[49*8:51*8].intbe 308 | ))) 309 | elif tag == 'f': 310 | fallback = parse_fallback(tagdata, addr.currency) 311 | if fallback: 312 | addr.tags.append(('f', fallback)) 313 | else: 314 | # Incorrect version. 315 | addr.unknown_tags.append((tag, tagdata)) 316 | continue 317 | 318 | elif tag == 'd': 319 | addr.tags.append(('d', trim_to_bytes(tagdata).decode('utf-8'))) 320 | 321 | elif tag == 'h': 322 | if data_length != 52: 323 | addr.unknown_tags.append((tag, tagdata)) 324 | continue 325 | addr.tags.append(('h', trim_to_bytes(tagdata))) 326 | 327 | elif tag == 'x': 328 | addr.tags.append(('x', tagdata.uint)) 329 | 330 | elif tag == 'p': 331 | if data_length != 52: 332 | addr.unknown_tags.append((tag, tagdata)) 333 | continue 334 | addr.paymenthash = trim_to_bytes(tagdata) 335 | 336 | elif tag == 'n': 337 | if data_length != 53: 338 | addr.unknown_tags.append((tag, tagdata)) 339 | continue 340 | addr.pubkey = secp256k1.PublicKey(flags=secp256k1.ALL_FLAGS) 341 | addr.pubkey.deserialize(trim_to_bytes(tagdata)) 342 | 343 | elif tag == 'c': 344 | addr.min_final_cltv_expiry = tagdata.uint 345 | else: 346 | addr.unknown_tags.append((tag, tagdata)) 347 | 348 | # BOLT #11: 349 | # 350 | # A reader MUST check that the `signature` is valid (see the `n` tagged 351 | # field specified below). 352 | if addr.pubkey: # Specified by `n` 353 | # BOLT #11: 354 | # 355 | # A reader MUST use the `n` field to validate the signature instead of 356 | # performing signature recovery if a valid `n` field is provided. 357 | addr.signature = addr.pubkey.ecdsa_deserialize_compact(sigdecoded[0:64]) 358 | if not addr.pubkey.ecdsa_verify(bytearray([ord(c) for c in hrp]) + data.tobytes(), addr.signature): 359 | raise ValueError('Invalid signature') 360 | else: # Recover pubkey from signature. 361 | addr.pubkey = secp256k1.PublicKey(flags=secp256k1.ALL_FLAGS) 362 | addr.signature = addr.pubkey.ecdsa_recoverable_deserialize( 363 | sigdecoded[0:64], sigdecoded[64]) 364 | addr.pubkey.public_key = addr.pubkey.ecdsa_recover( 365 | bytearray([ord(c) for c in hrp]) + data.tobytes(), addr.signature) 366 | 367 | return addr 368 | -------------------------------------------------------------------------------- /lnd.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | from lnaddr import lndecode 3 | from utils import TailableProc, BITCOIND_CONFIG 4 | import rpc_pb2_grpc as lnrpc_grpc 5 | import rpc_pb2 as lnrpc 6 | from ephemeral_port_reserve import reserve 7 | 8 | 9 | import grpc 10 | import logging 11 | import os 12 | import time 13 | import codecs 14 | 15 | 16 | # Needed for grpc to negotiate a valid cipher suite 17 | os.environ["GRPC_SSL_CIPHER_SUITES"] = "ECDHE-ECDSA-AES256-GCM-SHA384" 18 | 19 | 20 | class LndD(TailableProc): 21 | 22 | CONF_NAME = 'lnd.conf' 23 | 24 | def __init__(self, lightning_dir, bitcoind, port): 25 | super().__init__(lightning_dir, 'lnd({})'.format(port)) 26 | self.lightning_dir = lightning_dir 27 | self.bitcoind = bitcoind 28 | self.port = port 29 | self.rpc_port = str(reserve()) 30 | self.rest_port = str(reserve()) 31 | self.prefix = 'lnd' 32 | 33 | self.cmd_line = [ 34 | 'bin/lnd', 35 | '--bitcoin.active', 36 | '--bitcoin.regtest', 37 | '--datadir={}'.format(lightning_dir), 38 | '--debuglevel=trace', 39 | '--rpclisten=127.0.0.1:{}'.format(self.rpc_port), 40 | '--restlisten=127.0.0.1:{}'.format(self.rest_port), 41 | '--listen=127.0.0.1:{}'.format(self.port), 42 | '--tlscertpath=tls.cert', 43 | '--tlskeypath=tls.key', 44 | '--bitcoin.node=bitcoind', 45 | '--bitcoind.rpchost=127.0.0.1:{}'.format(BITCOIND_CONFIG.get('rpcport', 18332)), 46 | '--bitcoind.rpcuser=rpcuser', 47 | '--bitcoind.rpcpass=rpcpass', 48 | '--bitcoind.zmqpubrawblock=tcp://127.0.0.1:{}'.format(self.bitcoind.zmqpubrawblock_port), 49 | '--bitcoind.zmqpubrawtx=tcp://127.0.0.1:{}'.format(self.bitcoind.zmqpubrawtx_port), 50 | '--configfile={}'.format(os.path.join(lightning_dir, self.CONF_NAME)), 51 | '--no-macaroons', 52 | '--nobootstrap', 53 | '--noseedbackup', 54 | '--trickledelay=500' 55 | ] 56 | 57 | if not os.path.exists(lightning_dir): 58 | os.makedirs(lightning_dir) 59 | with open(os.path.join(lightning_dir, self.CONF_NAME), "w") as f: 60 | f.write("""[Application Options]\n""") 61 | 62 | def start(self): 63 | super().start() 64 | self.wait_for_log('RPC server listening on') 65 | self.wait_for_log('Done catching up block hashes') 66 | time.sleep(5) 67 | 68 | logging.info('LND started (pid: {})'.format(self.proc.pid)) 69 | 70 | def stop(self): 71 | self.proc.terminate() 72 | time.sleep(3) 73 | if self.proc.poll() is None: 74 | self.proc.kill() 75 | self.proc.wait() 76 | super().save_log() 77 | 78 | 79 | class LndNode(object): 80 | 81 | displayName = 'lnd' 82 | 83 | def __init__(self, lightning_dir, lightning_port, bitcoind, executor=None, node_id=0): 84 | self.bitcoin = bitcoind 85 | self.executor = executor 86 | self.daemon = LndD(lightning_dir, bitcoind, port=lightning_port) 87 | self.rpc = LndRpc(self.daemon.rpc_port) 88 | self.logger = logging.getLogger('lnd-node({})'.format(lightning_port)) 89 | self.myid = None 90 | self.node_id = node_id 91 | 92 | def id(self): 93 | if not self.myid: 94 | self.myid = self.info()['id'] 95 | return self.myid 96 | 97 | def ping(self): 98 | """ Simple liveness test to see if the node is up and running 99 | 100 | Returns true if the node is reachable via RPC, false otherwise. 101 | """ 102 | try: 103 | self.rpc.stub.GetInfo(lnrpc.GetInfoRequest()) 104 | return True 105 | except Exception as e: 106 | print(e) 107 | return False 108 | 109 | def peers(self): 110 | peers = self.rpc.stub.ListPeers(lnrpc.ListPeersRequest()).peers 111 | return [p.pub_key for p in peers] 112 | 113 | def check_channel(self, remote): 114 | """ Make sure that we have an active channel with remote 115 | """ 116 | self_id = self.id() 117 | remote_id = remote.id() 118 | channels = self.rpc.stub.ListChannels(lnrpc.ListChannelsRequest()).channels 119 | channel_by_remote = {c.remote_pubkey: c for c in channels} 120 | if remote_id not in channel_by_remote: 121 | self.logger.warning("Channel {} -> {} not found".format(self_id, remote_id)) 122 | return False 123 | 124 | channel = channel_by_remote[remote_id] 125 | self.logger.debug("Channel {} -> {} state: {}".format(self_id, remote_id, channel)) 126 | return channel.active 127 | 128 | def addfunds(self, bitcoind, satoshis): 129 | req = lnrpc.NewAddressRequest(type=1) 130 | addr = self.rpc.stub.NewAddress(req).address 131 | btc_addr = bitcoind.rpc.getnewaddress() 132 | bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) 133 | self.daemon.wait_for_log("Inserting unconfirmed transaction") 134 | bitcoind.rpc.generatetoaddress(1, btc_addr) 135 | self.daemon.wait_for_log("Marking unconfirmed transaction") 136 | 137 | # The above still doesn't mean the wallet balance is updated, 138 | # so let it settle a bit 139 | i = 0 140 | while self.rpc.stub.WalletBalance(lnrpc.WalletBalanceRequest()).total_balance == satoshis and i < 30: 141 | time.sleep(1) 142 | i += 1 143 | assert(self.rpc.stub.WalletBalance(lnrpc.WalletBalanceRequest()).total_balance == satoshis) 144 | 145 | def openchannel(self, node_id, host, port, satoshis): 146 | peers = self.rpc.stub.ListPeers(lnrpc.ListPeersRequest()).peers 147 | peers_by_pubkey = {p.pub_key: p for p in peers} 148 | if node_id not in peers_by_pubkey: 149 | raise ValueError("Could not find peer {} in peers {}".format(node_id, peers)) 150 | peer = peers_by_pubkey[node_id] 151 | self.rpc.stub.OpenChannel(lnrpc.OpenChannelRequest( 152 | node_pubkey=codecs.decode(peer.pub_key, 'hex_codec'), 153 | local_funding_amount=satoshis, 154 | push_sat=0 155 | )) 156 | 157 | # Somehow broadcasting a tx is slow from time to time 158 | time.sleep(5) 159 | 160 | def getchannels(self): 161 | req = lnrpc.ChannelGraphRequest() 162 | rep = self.rpc.stub.DescribeGraph(req) 163 | channels = [] 164 | 165 | for e in rep.edges: 166 | channels.append((e.node1_pub, e.node2_pub)) 167 | channels.append((e.node2_pub, e.node1_pub)) 168 | return channels 169 | 170 | def getnodes(self): 171 | req = lnrpc.ChannelGraphRequest() 172 | rep = self.rpc.stub.DescribeGraph(req) 173 | nodes = set([n.pub_key for n in rep.nodes]) - set([self.id()]) 174 | return nodes 175 | 176 | def invoice(self, amount): 177 | req = lnrpc.Invoice(value=int(amount/1000)) 178 | rep = self.rpc.stub.AddInvoice(req) 179 | return rep.payment_request 180 | 181 | def send(self, bolt11): 182 | req = lnrpc.SendRequest(payment_request=bolt11) 183 | res = self.rpc.stub.SendPaymentSync(req) 184 | if res.payment_error: 185 | raise ValueError(res.payment_error) 186 | return hexlify(res.payment_preimage) 187 | 188 | def connect(self, host, port, node_id): 189 | addr = lnrpc.LightningAddress(pubkey=node_id, host="{}:{}".format(host, port)) 190 | req = lnrpc.ConnectPeerRequest(addr=addr, perm=True) 191 | logging.debug(self.rpc.stub.ConnectPeer(req)) 192 | 193 | def info(self): 194 | r = self.rpc.stub.GetInfo(lnrpc.GetInfoRequest()) 195 | return { 196 | 'id': r.identity_pubkey, 197 | 'blockheight': r.block_height, 198 | } 199 | 200 | def block_sync(self, blockhash): 201 | print("Waiting for node to learn about", blockhash) 202 | self.daemon.wait_for_log('NTFN: New block: height=([0-9]+), sha={}'.format(blockhash)) 203 | 204 | def restart(self): 205 | self.daemon.stop() 206 | time.sleep(5) 207 | self.daemon.start() 208 | self.rpc = LndRpc(self.daemon.rpc_port) 209 | 210 | def stop(self): 211 | self.daemon.stop() 212 | 213 | def start(self): 214 | self.daemon.start() 215 | self.rpc = LndRpc(self.daemon.rpc_port) 216 | 217 | def check_route(self, node_id, amount): 218 | try: 219 | req = lnrpc.QueryRoutesRequest(pub_key=node_id, amt=int(amount/1000), num_routes=1) 220 | r = self.rpc.stub.QueryRoutes(req) 221 | except grpc._channel._Rendezvous as e: 222 | if (str(e).find("unable to find a path to destination") > 0): 223 | return False 224 | raise 225 | return True 226 | 227 | class LndRpc(object): 228 | 229 | def __init__(self, rpc_port): 230 | self.port = rpc_port 231 | cred = grpc.ssl_channel_credentials(open('tls.cert', 'rb').read()) 232 | channel = grpc.secure_channel('localhost:{}'.format(rpc_port), cred) 233 | self.stub = lnrpc_grpc.LightningStub(channel) 234 | -------------------------------------------------------------------------------- /logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | System.out 22 | false 23 | 24 | %yellow(${HOSTNAME} %d) %highlight(%-5level) %logger{24} %X{nodeId}%X{channelId} - %msg%ex{12}%n 25 | 26 | 27 | 28 | 29 | ${eclair.datadir:-${user.home}/.eclair}/eclair.log 30 | true 31 | 32 | %d %-5level %logger{24} %X{nodeId}%X{channelId} - %msg%ex{24}%n 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | %d %-5level %logger{24} %X{nodeId}%X{channelId} - %msg%ex{24}%n 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdecker/lightning-integration/4e66f74f68b28efeac437b3e104fe00b75d73e05/output/.gitkeep -------------------------------------------------------------------------------- /ptarmd.py: -------------------------------------------------------------------------------- 1 | from utils import TailableProc 2 | 3 | import json 4 | import logging 5 | import os 6 | import time 7 | import subprocess 8 | import re 9 | import socket 10 | 11 | 12 | class PtarmD(TailableProc): 13 | 14 | def __init__(self, lightning_dir, bitcoin_dir, port=9735): 15 | TailableProc.__init__(self, lightning_dir, 'ptarmd({}).format(port)') 16 | self.lightning_dir = lightning_dir 17 | self.port = port 18 | self.cmd_line = [ 19 | 'bin/ptarmd', 20 | '-d', lightning_dir, 21 | '-p', str(port), 22 | '-c', '{}/bitcoin.conf'.format(bitcoin_dir), 23 | '--network', 'regtest', 24 | '--rpcport', str(port+1234), 25 | ] 26 | self.prefix = 'ptarmd' 27 | 28 | if not os.path.exists(lightning_dir): 29 | os.makedirs(lightning_dir) 30 | 31 | def start(self): 32 | TailableProc.start(self) 33 | self.wait_for_log("start ptarmigan node.", offset=100) 34 | time.sleep(3) 35 | logging.info("PtarmD started") 36 | 37 | def stop(self): 38 | TailableProc.stop(self) 39 | logging.info("PtarmD stopped") 40 | 41 | 42 | class PtarmNode(object): 43 | 44 | displayName = 'ptarmigan' 45 | 46 | def __init__(self, lightning_dir, lightning_port, btc, executor=None, 47 | node_id=0): 48 | self.bitcoin = btc 49 | self.executor = executor 50 | self.daemon = PtarmD( 51 | lightning_dir, 52 | btc.bitcoin_dir, 53 | port=lightning_port 54 | ) 55 | self.rpc = PtarmRpc('127.0.0.1', lightning_port+1234) 56 | self.myid = None 57 | self.node_id = node_id 58 | self.bitcoind = None 59 | self.txid = None 60 | self.vout = None 61 | self.peer_host = None 62 | self.peer_port = None 63 | self.peer_node_id = None 64 | self.push_sat = 0 65 | self.feerate_per_kw = 12*1000 66 | self.logger = self.daemon.logger 67 | 68 | def peers(self): 69 | r = self.rpc.getinfo() 70 | return [p['node_id'] for p in r['peers']] 71 | 72 | def getinfo(self): 73 | raise NotImplementedError() 74 | 75 | def id(self): 76 | if not self.myid: 77 | self.myid = self.rpc.getinfo()['node_id'] 78 | return self.myid 79 | 80 | def openchannel(self, node_id, host, port, satoshis): 81 | # Make sure we have a connection already 82 | if node_id not in self.peers(): 83 | raise ValueError("Must connect to node before opening a channel") 84 | return self.rpc.fundchannel( 85 | node_id, 86 | self.peer_host, 87 | self.peer_port, 88 | self.txid, 89 | self.vout, 90 | satoshis, 91 | self.push_sat, 92 | self.feerate_per_kw 93 | ) 94 | 95 | def getaddress(self): 96 | raise NotImplementedError() 97 | 98 | def addfunds(self, bitcoind, satoshis): 99 | # ptarmd uses bitcoind's wallet. 100 | self.bitcoind = bitcoind 101 | addr = bitcoind.rpc.getnewaddress('', 'p2sh-segwit') 102 | self.txid = bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) 103 | listunspent = bitcoind.rpc.listunspent(0, 1, [addr]) 104 | self.vout = listunspent[0]['vout'] 105 | 106 | # Lock vout to not be used for other transactions. 107 | assert bitcoind.rpc.lockunspent( 108 | False, 109 | [{"txid": self.txid, "vout": self.vout}] 110 | ) 111 | 112 | time.sleep(1) 113 | bitcoind.rpc.generatetoaddress(1, addr) 114 | 115 | def ping(self): 116 | """ Simple liveness test to see if the node is up and running 117 | 118 | Returns true if the node is reachable via RPC, false otherwise. 119 | """ 120 | try: 121 | self.rpc.getinfo() 122 | return True 123 | except Exception: 124 | return False 125 | 126 | def check_channel(self, remote): 127 | """ Make sure that we have an active channel with remote 128 | """ 129 | remote_id = remote.id() 130 | self_id = self.id() 131 | for p in self.rpc.getinfo()['peers']: 132 | if 'node_id' not in p: 133 | continue 134 | if remote.id() == p['node_id']: 135 | state = p['status'] 136 | self.logger.debug("Channel {} -> {} state: {}".format( 137 | self_id, 138 | remote_id, state 139 | )) 140 | return state == 'normal operation' 141 | 142 | self.logger.warning("Channel {} -> {} not found".format( 143 | self_id, 144 | remote_id 145 | )) 146 | return False 147 | 148 | def getchannels(self): 149 | proc = subprocess.run( 150 | ['{}/bin/showdb'.format(os.getcwd()), '-c'], 151 | stdout=subprocess.PIPE, 152 | cwd=self.daemon.lightning_dir 153 | ) 154 | decoder = json.JSONDecoder() 155 | objs, _ = decoder.raw_decode(proc.stdout.decode("UTF-8")) 156 | result = [] 157 | if 'channel_announcement_list' in objs: 158 | for c in objs['channel_announcement_list']: 159 | if c['type'] != 'channel_announcement': 160 | continue 161 | result.append((c['node1'], c['node2'])) 162 | result.append((c['node2'], c['node1'])) 163 | return set(result) 164 | 165 | def getnodes(self): 166 | """ Get nodes on channels 167 | """ 168 | nodes = set() 169 | 170 | # Get a list of node ids from `node_announcement`s. but it 171 | # always includes my node id even if my node has no relevant 172 | # channels. 173 | proc = subprocess.run( 174 | ['{}/bin/showdb'.format(os.getcwd()), '-n'], 175 | stdout=subprocess.PIPE, 176 | cwd=self.daemon.lightning_dir 177 | ) 178 | objs, _ = json.JSONDecoder().raw_decode(proc.stdout.decode("UTF-8")) 179 | if 'node_announcement_list' not in objs: 180 | return set() 181 | nodes = set([n['node'] for n in objs['node_announcement_list']]) 182 | 183 | # Get a list of `channel_announcement`s, 184 | # and discard my node id from `nodes` if it has no relevant channels. 185 | proc = subprocess.run( 186 | ['{}/bin/showdb'.format(os.getcwd()), '-c'], 187 | stdout=subprocess.PIPE, 188 | cwd=self.daemon.lightning_dir 189 | ) 190 | objs, _ = json.JSONDecoder().raw_decode(proc.stdout.decode("UTF-8")) 191 | if 'channel_announcement_list' not in objs: 192 | return set() 193 | for c in objs['channel_announcement_list']: 194 | if c['type'] != 'channel_announcement': 195 | continue 196 | if c['node1'] == self.id(): 197 | # found 198 | break 199 | if c['node2'] == self.id(): 200 | # found 201 | break 202 | else: 203 | # not found 204 | nodes.discard(self.id()) 205 | 206 | return nodes 207 | 208 | def invoice(self, amount): 209 | r = self.rpc.invoice(amount) 210 | return r['bolt11'] 211 | 212 | def send(self, req): 213 | self.rpc.pay(req) # Will raise on error, but no useful info 214 | 215 | # Grab the preimage from listpayment 216 | preimage = None 217 | for i in range(5): 218 | r = self.rpc.listpayment() 219 | for pay in r: 220 | if pay['invoice'] == req: 221 | if ('preimage' in pay) and (len(pay['preimage']) != 0): 222 | preimage = pay['preimage'] 223 | break 224 | if preimage is None: 225 | time.sleep(1) 226 | continue 227 | break 228 | if preimage is None: 229 | raise ValueError( 230 | "Could not found preimage from listpayment" 231 | ) 232 | return preimage 233 | 234 | def connect(self, host, port, node_id): 235 | self.peer_host = host 236 | self.peer_port = port 237 | self.peer_node_id = node_id 238 | initial_routing_sync = 1 239 | return self.rpc.connect(node_id, host, port, initial_routing_sync) 240 | 241 | def info(self): 242 | r = self.rpc.getinfo() 243 | return { 244 | 'id': r['node_id'], 245 | 'blockheight': r['block_count'], 246 | } 247 | 248 | def block_sync(self, blockhash): 249 | time.sleep(1) 250 | 251 | def restart(self): 252 | self.daemon.stop() 253 | time.sleep(5) 254 | self.daemon.start() 255 | time.sleep(1) 256 | 257 | def stop(self): 258 | self.daemon.stop() 259 | 260 | def start(self): 261 | self.daemon.start() 262 | 263 | def check_route(self, node_id, amount): 264 | proc = subprocess.run([ 265 | '{}/bin/routing'.format(os.getcwd()), 266 | '-s', 267 | self.id(), 268 | '-r', 269 | node_id, 270 | '-a', 271 | str(amount) 272 | ], stdout=subprocess.PIPE, cwd=self.daemon.lightning_dir) 273 | return proc.returncode == 0 274 | 275 | 276 | class TcpSocketRpc(object): 277 | # The code of this class was copied a lot from `lightning.py` 278 | # - https://github.com/ElementsProject/lightning/blob/master/contrib/pylightning/lightning/lightning.py 279 | 280 | def __init__(self, host, port, executor=None, logger=logging): 281 | self.host = host 282 | self.port = port 283 | self.decoder = json.JSONDecoder() 284 | self.executor = executor 285 | self.logger = logger 286 | 287 | @staticmethod 288 | def _writeobj(sock, obj): 289 | s = json.dumps(obj) 290 | sock.sendall(bytearray(s, 'UTF-8')) 291 | 292 | def _readobj(self, sock): 293 | buff = b'' 294 | while True: 295 | try: 296 | b = sock.recv(1024) 297 | buff += b 298 | if len(b) == 0: 299 | return {'error': 'Connection to RPC server lost.'} 300 | # Convert late to UTF-8 so glyphs split across recvs do not 301 | # impact us 302 | objs, _ = self.decoder.raw_decode(buff.decode("UTF-8")) 303 | return objs 304 | except ValueError: 305 | # Probably didn't read enough 306 | pass 307 | 308 | def __getattr__(self, name): 309 | """Intercept any call that is not explicitly defined and call @call 310 | 311 | We might still want to define the actual methods in the subclasses for 312 | documentation purposes. 313 | """ 314 | name = name.replace('_', '-') 315 | 316 | def wrapper(**kwargs): 317 | return self.call(name, payload=kwargs) 318 | return wrapper 319 | 320 | def call(self, method, payload=None): 321 | self.logger.debug("Calling %s with payload %r", method, payload) 322 | 323 | if payload is None: 324 | payload = {} 325 | # Filter out arguments that are None 326 | payload = [v for v in payload if v is not None] 327 | 328 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 329 | sock.connect((self.host, self.port)) 330 | self._writeobj(sock, { 331 | "method": method, 332 | "params": payload, 333 | "id": 0 334 | }) 335 | resp = self._readobj(sock) 336 | sock.close() 337 | 338 | self.logger.debug("Received response for %s call: %r", method, resp) 339 | if "error" in resp: 340 | raise ValueError( 341 | "RPC call failed: {}, method: {}, payload: {}".format( 342 | resp["error"], 343 | method, 344 | payload 345 | )) 346 | elif "result" not in resp: 347 | raise ValueError("Malformed response, \"result\" missing.") 348 | return resp["result"] 349 | 350 | 351 | class PtarmRpc(TcpSocketRpc): 352 | 353 | def invoice(self, msatoshi): 354 | payload = [msatoshi] 355 | return self.call("invoice", payload) 356 | 357 | def getinfo(self): 358 | return self.call("getinfo") 359 | 360 | def listpayment(self): 361 | return self.call("listpayment") 362 | 363 | def pay(self, bolt11, msatoshi=None, description=None, riskfactor=None): 364 | payload = [bolt11, 0] 365 | return self.call("routepay", payload) 366 | 367 | def connect(self, peer_id, host=None, port=None, initial_routing_sync=None): 368 | payload = [peer_id, '127.0.0.1', port, initial_routing_sync] 369 | return self.call("connect", payload) 370 | 371 | def fundchannel(self, peer_id, peer_host, peer_port, txid, txindex, 372 | funding_sat, push_sat, feerate_per_kw): 373 | payload = [ 374 | peer_id, 375 | peer_host, 376 | peer_port, 377 | txid, 378 | txindex, 379 | funding_sat, 380 | push_sat, 381 | feerate_per_kw 382 | ] 383 | return self.call("fund", payload) 384 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::UserWarning 4 | ignore::ResourceWarning 5 | -------------------------------------------------------------------------------- /reports/07f1821c5629d6eaf2df351ce27fc9c30cdd4fb0d3e8fb6fe76ffe1194bc28b7.json: -------------------------------------------------------------------------------- 1 | {"versions": {"eclair": "d0a18c064993ea650a4ddcc6704ded08143d3100", "lightning": "f085a474b2d197d342ebe81a542c63f669eac78a", "lnd": "e6984c42027c743e05ceb17bf48724d12f376c74"}, "summary": {"num_tests": 30, "passed": 18, "failed": 12, "duration": 737.6639785766602}, "id": "07f1821c5629d6eaf2df351ce27fc9c30cdd4fb0d3e8fb6fe76ffe1194bc28b7", "environment": {"Platform": "Linux-4.4.0-75-generic-x86_64-with-Ubuntu-16.04-xenial", "Python": "3.5.2"}, "tests": [{"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.408205509185791}, "outcome": "passed", "setup": {"duration": 1.4342565536499023, "name": "setup", "outcome": "passed", "stdout": "{\"method\": \"getinfo\", \"params\": [], \"id\": 1, \"version\": \"1.1\"}\n"}, "name": "test.py::testStart[EclairNode]", "call": {"name": "call", "outcome": "passed", "duration": 5.752195119857788}, "run_index": 0, "duration": 9.028913736343384}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0004553794860839844}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0006694793701171875}, "name": "test.py::testStart[LightningNode]", "call": {"name": "call", "outcome": "passed", "duration": 0.4649314880371094}, "run_index": 1, "duration": 0.46672582626342773}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.001725912094116211}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0006837844848632812}, "name": "test.py::testStart[LndNode]", "call": {"name": "call", "outcome": "passed", "duration": 9.804884672164917}, "run_index": 2, "duration": 9.80797815322876}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.8947844505310059}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0025529861450195312}, "name": "test.py::testOpenchannel[EclairNode_EclairNode]", "call": {"duration": 19.65378189086914, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"mp39Tx4txsWjLqPSLefaUSKwTvB5ecTmyH\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 12, "duration": 20.553672313690186}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.49302148818969727}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0006387233734130859}, "name": "test.py::testgossip[LndNode_EclairNode]", "call": {"duration": 52.348432779312134, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14af7f60d0>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2MvoUSq5iDrt2E8QLsiFttXDrdBBs1AzoGx\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"d54da11783fdbaa489e42a85483662a17c61875cb4b6d94934f0f5ed82742ce6\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N56Go4Tc7tVPz7yrAF74Qve2W24aeRftgp\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"93e000ab6e674c12d04026f833fd7e94e2decca1f73f4b870901437f21597833\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N5baZfhecoeGEGzzLfCDomrJKJTiF5eCY3\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"9e9dde68f0a841eb7dc1a35d7325381dcebb5632c0ae6e8ea39a28a08b70f872\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N2EEfixh9ds4S717xqRUQdejkHKmnRV4YP\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"8722e72487d122e4d2909ce26dcab3df978b0d87bcbc5251f1b416906c487f1f\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 27, "duration": 52.84273171424866}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.41069984436035156}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0019989013671875}, "name": "test.py::testConnect[LndNode_EclairNode]", "call": {"duration": 15.398305892944336, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 03c8979501ebe32fc3e53a529736b740975d46aa71e310cba8d3ffccee74670e82@localhost:16331 -> 02f517f13fb42d9ef9ccf3ed0cdccb40222b2208d1382aa3488af3dae800f1fd68@localhost:16332\n"}, "run_index": 9, "duration": 15.813003540039062}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.856332540512085}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.002754688262939453}, "name": "test.py::testConnect[EclairNode_EclairNode]", "call": {"duration": 9.363102674484253, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 03053532d4e9ec8fd0269b6ec2f9c33cd875a9613bd13018a60fbd6f35d6fa8aa2@localhost:16331 -> 02712989d00ce902a9a400e2cc35226070de1a73d201fd7680d8bd9faa97263ba8@localhost:16332\n"}, "run_index": 3, "duration": 10.224944591522217}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.012206077575683594}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0011115074157714844}, "name": "test.py::testgossip[LightningNode_LightningNode]", "call": {"duration": 9.230309009552002, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2MxQELs66Tu5RF5txLi724g94w49bY6M6R4\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"7846e20d78b92a098f1ebe582081a0433e678c5abc4148a63dd4af475cd5e746\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2MyKoiXCdTMjjztD7QqLQqQ1uPQG6ACQSPL\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"7b75c38259f0776f43c65c8e1fedac8c72b166a73910efe345c97f090f6fac0d\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N2xgoep7T7Rgo7Yr4m3ArCuNSWkihupRGk\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"861342f4d7f875fbc6dca8b4c048067603d888ab38073f91d7a2d354dedabe72\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NFZYRJfQJRrjWh7tyLKjAbyEtcXC8XtiLs\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"cbd691689b6458dfad25dedeaa2ad2b7040dd316ccc968ddc42e4fdf1dccd137\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 25, "duration": 9.244738101959229}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.39641284942626953}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0009794235229492188}, "name": "test.py::testOpenchannel[EclairNode_LndNode]", "call": {"duration": 33.20130920410156, "name": "call", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d45e5d90>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"mvJoQjtRqWHpPaQspVKLzhYCGn5QqqiRAs\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 14, "duration": 33.59968090057373}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0009794235229492188}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0021936893463134766}, "name": "test.py::testOpenchannel[LightningNode_LndNode]", "call": {"duration": 12.662590026855469, "name": "call", "longrepr": "test.py:155: in testOpenchannel\n node1.openchannel(node2.id(), 'localhost', node2.daemon.port, 10**7)\nlightningd.py:69: in openchannel\n return self.rpc.fundchannel(node_id, satoshis)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:44: in wrapper\n return self._call(name, args)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:62: in _call\n raise ValueError(\"RPC call failed: {}\".format(resp['error']))\nE ValueError: RPC call failed: Peer died", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N5fhVbX8zcZGeRFbMafgjD2ykNsUJQDZxy\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"6d49fc467f1ce3494ba2e392e763811e7870017d596d9df31e9b355bb21c462d\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 17, "duration": 12.667956829071045}, {"teardown": {"duration": 0.38188815116882324, "name": "teardown", "outcome": "passed", "stdout": "{\"method\": \"stop\", \"params\": [], \"id\": 1, \"version\": \"1.1\"}\n"}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0008907318115234375}, "name": "test.py::testgossip[LndNode_LndNode]", "call": {"duration": 57.147869348526, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14aed64e18>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N61FpooQ4SzwqgYuXd6YoU37pmJ3NcGeiH\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"e059a6e4d6bd59a4a8c2a0b5aaefe5a5cc31cf3f4cdeb9f3b98ea64647238e63\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NE2P9p51AQXATPaxeaE55hezijShR5CNvY\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"396eaf95e9d9ebd43f086cac98088e366be5855ef758246526618d2a5e3ca2ba\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N64MpkUJP7D2GrnQnchCpFAV1obVfRGmv4\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"6606b760549bbf1e8615857ec58a5b2eadfd2f949de110973c1bf6fbadf3afb6\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NFYnx4KFoBFPwUkonvPyTTnYWVQs5Pkade\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"14384706ae6ab3f514c886cbf9279f7b3842969df7b13c7acf0d9652bdf40e12\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 29, "duration": 57.53153896331787}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.41248321533203125}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0011186599731445312}, "name": "test.py::testgossip[LightningNode_EclairNode]", "call": {"duration": 43.44151854515076, "name": "call", "longrepr": "test.py:198: in testgossip\n wait_for(lambda: len(node2.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d41f28c8>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N7BcKzsBqb4to5mh5LYEjxEm13RSFPpTgt\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"0583858f430e52b68fe9efa3197b6becbd9689a91f10cd6882b804a06d68e487\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N612i99d9eTdkQuzVVtTixwZw4CftPz1sp\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"ee6bf6e4e621148757c10b40a957b105be300f09c6aff82ba3e810ea41a10aa9\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NGQQihHDPxXRP5aaKQDh5B18T6mH116YHh\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"ff63f82263d665ad1157d150288c3c9ee8e03f1c7fc9c45de5a83569aacd544b\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NF6aCZEDzvdqFSiEtjQBLJSE18oQFJB14s\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"64a1cb5bd11675b88a3e99cdb414d8eff472df2ce58b1cd0539884d8b1917e18\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 24, "duration": 43.85623908042908}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.434276819229126}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0008721351623535156}, "name": "test.py::testConnect[LightningNode_EclairNode]", "call": {"duration": 5.413315296173096, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 03fadf7a23eebb1663d2804feb65a7e7969fb0e03ccf1e45fe6bdf1c1002e4b20f@localhost:16331 -> 03b6cebd5a3b2b398893bb149f49d4b2fc884a80a37b07d8541871134032d478ac@localhost:16332\n"}, "run_index": 6, "duration": 5.849336385726929}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0007004737854003906}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0009393692016601562}, "name": "test.py::testOpenchannel[LndNode_LightningNode]", "call": {"duration": 35.762476444244385, "name": "call", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d4561e18>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N6DCpTdDCJ3p7yPpzAVSqjc8GSiaKjRKXG\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 19, "duration": 35.765055656433105}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.3973081111907959}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0009720325469970703}, "name": "test.py::testOpenchannel[LndNode_EclairNode]", "call": {"duration": 44.559107065200806, "name": "call", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d4536f28>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N1J18yaitV6GeCZiLueZtjgktxR8HRFcW5\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 18, "duration": 44.958359241485596}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.4277927875518799}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0006220340728759766}, "name": "test.py::testgossip[EclairNode_LndNode]", "call": {"duration": 52.77761912345886, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d41f2b70>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2Mx3WVNjXxVoUUyMTeVHHyB59pATB9qpCGW\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"f16d3c85cbbabbb777136c881d7cc0b7191c92b1a6fd0b6df38636f2748c7d2e\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2Msn7ja2errNNConmPe1bL6NCUyck5dE7gf\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"2a572f726977957716ee812544601d9850ec6d6c5ca576a62f65f8bc683adb40\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N5yHXwPLL65XKpLyGXQj4y7tBxRihWV9SU\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"a12f9c6f492497a1dcf1ff84c577c0e72a0d9a6fa578cf55da374c0f86eadc2c\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NB9VCyUzA8mUY7Ufc6ASLvKt6LarUiaAXr\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"d7162a58b4a7bd9665230ab8702bbf26d3c78ee4aebe067f74598cc6fe1fbf5c\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 23, "duration": 53.206655979156494}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.8410627841949463}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0019044876098632812}, "name": "test.py::testgossip[EclairNode_EclairNode]", "call": {"duration": 46.62866449356079, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d45dcf28>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2NCu8aUfURAyChQFoyc3hDZ4dqq1UMJ8Drd\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"59cbd4e9fb05f3ad9afaf675002d9819e7b279f2ae0c7b1aa5a3c55914e14c5b\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NBmNmDqmSk8ZPPP8vwCZrs4SdJ29kXmmSk\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"2e43d1b2db687dd83a440d46af7a0d5f1041f24b7222f857bf74f19e9edff85c\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2MwYm9uG1GqmF23AumDMhMKH5ykjEAL1yAd\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"d903e182bf1c0b51fa90f270d9fd16822f229322d4904c55ab1456f131578fd7\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2MuUKkxKjys41FLxyPvSYGZme8J5mNYQzbp\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"63efa87848ecc7d367abfda9aaea31a4ceaee557313c157e984c16307a66b4d4\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 21, "duration": 47.473536252975464}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.4354269504547119}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0019307136535644531}, "name": "test.py::testConnect[EclairNode_LightningNode]", "call": {"duration": 5.548787832260132, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 02f24dcffb3b1a4f87d0d26bbddce398a1c9ff0863095da1d29dd53b88fcbc56b7@localhost:16331 -> 03c5ddc6f2692206ee07fb511f8d30df3b1948ec266b6cca7bc35d04877aec52b9@localhost:16332\n"}, "run_index": 4, "duration": 5.988076210021973}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.002168893814086914}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.002942323684692383}, "name": "test.py::testConnect[LndNode_LndNode]", "call": {"duration": 19.757091760635376, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 027550292081b62dc03efabf2e7d145607c66944746d388346095029e9e39ea4f8@localhost:16331 -> 02a4ecd0b30995bfeb57b84b3a31f78a7378ee1143579075bbffd0ac9eea273a91@localhost:16332\n"}, "run_index": 11, "duration": 19.765145301818848}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.38931775093078613}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0018892288208007812}, "name": "test.py::testConnect[EclairNode_LndNode]", "call": {"duration": 14.160849809646606, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 0268edfbdc71e75967337edcb0312ca9c8ce4205af58435ca1a9a60427f95c0ce1@localhost:16331 -> 02509fb5a88585ce559c598d7dfcc432e6b5446ae965813b25ce2f8b24b6876852@localhost:16332\n"}, "run_index": 5, "duration": 14.553946018218994}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0018198490142822266}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0020313262939453125}, "name": "test.py::testConnect[LndNode_LightningNode]", "call": {"duration": 10.421906232833862, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 028860d97fd0417d8ff73870ca83d1f1e27002e305719d29799270e6d0b79ea9d6@localhost:16331 -> 02d2ff8b389c9b91ddc7c5f784c21cdaa102a5c2652d406d5435d63fa272a3318b@localhost:16332\n"}, "run_index": 10, "duration": 10.427788734436035}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0187990665435791}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0008006095886230469}, "name": "test.py::testgossip[LndNode_LightningNode]", "call": {"duration": 47.87441825866699, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d4509268>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2NCG2Niphfq5bmo5Pv135suCfb1yA745i6A\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"725de34ae06c1dfeebf7316fa813ee21eac1134e8e0b0077cbb9c33a9fb865cd\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2MseNnWjYKQwp42nUyyzHLTxT5VsMLABvPQ\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"532842942843d5f8b707a5b2776b8268aee273e511e5fa84dceb0587b860422e\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NGAZuBBzW9CNWFGsJhYKxXqsvQWe2SHWBc\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"4e5af67f14c462cb9e87b5918f03cf5c77d8e0dd9c49e8779bd7cc9eae3f03b1\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NCCk3dGvFppmtWV3gnDKrGDGE5VY4mPQV3\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"187593d924f2d7d8a4160eac987222286a630124a97776a5381fd6754c70d45a\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 28, "duration": 47.89481854438782}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.4233434200286865}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.002033710479736328}, "name": "test.py::testgossip[EclairNode_LightningNode]", "call": {"duration": 43.63634490966797, "name": "call", "longrepr": "test.py:193: in testgossip\n wait_for(lambda: len(node1.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d45dc840>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2MyGP3KxDsysH741aebNwG653QcqHytz9cv\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"c219365014fcc723b2d80ca8cc29f064bd9ed1060ab3033ffee19d67aadc806b\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N1r2iwvZgNCyfALckKGG4EjK9wBzLYUnkR\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"41d286e71fdc5865297aebf8112cff565e79e51bd84fb1debc13f52010485cb8\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NAuxYT5iJUNKpWiXtdPfSTyiooq4fGGGdv\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"4bdfb50c599079f0a625dea4ea57793905e01bbb8019b0f1e4524e80d426e791\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NEynUJwceyu6KaNZg2S6r9Ch5RBAMHU9gZ\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"2f42b73a78d44f4223674387542b8de13db39442bfab512c6549e92cc942040d\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 22, "duration": 44.06375575065613}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.002399444580078125}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0012869834899902344}, "name": "test.py::testOpenchannel[LightningNode_LightningNode]", "call": {"duration": 8.74604320526123, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2Mt4234UjzJduuM3QwG9GtBzWtCuC9397Rf\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"9407202aaf1ceb22fd11378729f23368a8f8d9f15dde07662014f8bd5dab8db2\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 16, "duration": 8.751016616821289}, {"teardown": {"stderr": "lightningd_handshake: Writing out status 32767 len 69: Broken pipe\n", "name": "teardown", "outcome": "passed", "duration": 0.030225753784179688}, "outcome": "failed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.004965543746948242}, "name": "test.py::testgossip[LightningNode_LndNode]", "call": {"duration": 49.02284574508667, "name": "call", "longrepr": "test.py:198: in testgossip\n wait_for(lambda: len(node2.getnodes()) == 5, interval=1)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f14d42768c8>)", "outcome": "failed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N31BLan4JYjB5EZpW5hn5xUSxZDfxD1rzg\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"3b9460dce587d46b2340ffe3e0a7f3882daaaf13c68f61a6c5a508cbe4bd3463\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2NEv9Gq71q1ioe78ynK8HTkRF7vdy2rZtfP\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"9976bb82db5edcec61f42488c761d1b4a36e9e9152a7ac5d61699287fa147f6f\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2N6yDwamxEve75Ym5afoC2iPVqwRrn8dWm2\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"0a3f8157c5ec4a3aad49857b2d2d39b55bb08eebc4a0f0f730a15b10843721d5\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"sendtoaddress\", \"params\": [\"2MyyCxEnFmWKzFk64JsLamGFhGdKoNF3HQg\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"f30438686eda02631efc717d88d68d144ae1cda821ca3880fbe5d3a0e0f20f69\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [6], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 26, "duration": 49.063002586364746}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.002069711685180664}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0008499622344970703}, "name": "test.py::testOpenchannel[LndNode_LndNode]", "call": {"duration": 35.88502788543701, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2NFgaVsvDSD6gFXR649mQXc32RYDP3KcQuU\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 20, "duration": 35.88879752159119}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.0017938613891601562}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.002034902572631836}, "name": "test.py::testConnect[LightningNode_LndNode]", "call": {"duration": 10.348991394042969, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 032ebb267e9903fd2fb978ed031e19aba81e0eea99b3510dc80fb3df8499c3bd8a@localhost:16331 -> 0249ae029f5903470fb70aa8482885d0a5cdb68a3a220494751ba1e135f8ea6626@localhost:16332\n"}, "run_index": 8, "duration": 10.354855060577393}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.43303370475769043}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0007374286651611328}, "name": "test.py::testOpenchannel[LightningNode_EclairNode]", "call": {"duration": 13.790236949920654, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"2N5RdRZFFRYhXhEd6jqNsrhNNU6n5wBpCMg\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"getrawtransaction\", \"params\": [\"b2bff437f48346131287e2635f1ef435ccc3588b1becabc39ab87f8bfbe0ead5\"], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 15, "duration": 14.224745512008667}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.4061117172241211}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.002437114715576172}, "name": "test.py::testOpenchannel[EclairNode_LightningNode]", "call": {"duration": 13.250360012054443, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"sendtoaddress\", \"params\": [\"mzAPYXEJ95JtPmv7keYXPnZBprAUA6tQRP\", 0.2], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\n"}, "run_index": 13, "duration": 13.661345958709717}, {"teardown": {"name": "teardown", "outcome": "passed", "duration": 0.002568960189819336}, "outcome": "passed", "setup": {"name": "setup", "outcome": "passed", "duration": 0.0009529590606689453}, "name": "test.py::testConnect[LightningNode_LightningNode]", "call": {"duration": 1.0625686645507812, "name": "call", "outcome": "passed", "stdout": "{\"method\": \"generate\", \"params\": [1], \"id\": 1, \"version\": \"1.1\"}\nConnecting 02f1dbafb6aa866cbde93fd6587ed64700dd7ab4743dfc864b67a71eee72f0ac7b@localhost:16331 -> 03cf19eb731ad3d7e159b5cd282b8057c93c5b2999b57c40bf908f0cd15fb3479b@localhost:16332\n"}, "run_index": 7, "duration": 1.0670435428619385}], "created_at": "2017-08-16 22:42:57.577808"} -------------------------------------------------------------------------------- /reports/55e9c14d4944c5c0277885eaa04c2ecb6aab85d6a70e94885bbbec2fec7c9851.json: -------------------------------------------------------------------------------- 1 | {"id": "55e9c14d4944c5c0277885eaa04c2ecb6aab85d6a70e94885bbbec2fec7c9851", "tests": [{"run_index": 7, "call": {"name": "call", "duration": 1.269446611404419, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 02d4ea62844dd07fd2774920b774393ee9d337f218c951007085bdbbd8f3c18fb5@localhost:16331 -> 03bee9dd32b32f02ecdcc3d2cf176add6ad85d1bfad4936f2f2664d4ae9420b442@localhost:16332\n", "outcome": "passed"}, "duration": 1.2713921070098877, "name": "test.py::testConnect[LightningNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.0008511543273925781, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0005471706390380859, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 12, "call": {"name": "call", "duration": 17.568127870559692, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"msUbQuVXsFsDrDAgbonWCLKF9msLVf4XYg\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "outcome": "passed"}, "duration": 18.516801595687866, "name": "test.py::testOpenchannel[EclairNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.9446592330932617, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0020072460174560547, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 5, "call": {"name": "call", "duration": 18.103718757629395, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 026832331e658013d7f9ed4b253ede40f4a8553f94f7084c36233c5885b273f2c3@localhost:16331 -> 0237262e648957aa52cc971c5b57b614f4eabb01f11f4a7ea669ff25cf1fbdf470@localhost:16332\n", "outcome": "passed"}, "duration": 18.505255937576294, "name": "test.py::testConnect[EclairNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.39991307258605957, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0008120536804199219, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 0, "call": {"name": "call", "duration": 7.081137418746948, "outcome": "passed"}, "duration": 10.151566982269287, "name": "test.py::testStart[EclairNode]", "teardown": {"name": "teardown", "duration": 0.4179675579071045, "outcome": "passed"}, "setup": {"name": "setup", "duration": 1.3262310028076172, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [], \"method\": \"getinfo\"}\n", "outcome": "passed"}, "outcome": "passed"}, {"run_index": 17, "call": {"name": "call", "duration": 15.867064714431763, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2Msw6RyYKZfvS4vFBprRFEN6qaSuSmswd3w\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [\"bd1071db059532f72e91939a6dbda125975f1a30d0cbbd1b1e6987848a2cc8f7\"], \"method\": \"getrawtransaction\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "longrepr": "test.py:155: in testOpenchannel\n node1.openchannel(node2.id(), 'localhost', node2.daemon.port, 10**7)\nlightningd.py:69: in openchannel\n return self.rpc.fundchannel(node_id, satoshis)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:44: in wrapper\n return self._call(name, args)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:62: in _call\n raise ValueError(\"RPC call failed: {}\".format(resp['error']))\nE ValueError: RPC call failed: Peer died", "outcome": "failed"}, "duration": 15.872354984283447, "name": "test.py::testOpenchannel[LightningNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.0009377002716064453, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0021762847900390625, "outcome": "passed"}, "outcome": "failed"}, {"run_index": 20, "call": {"name": "call", "duration": 41.4880096912384, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2Mwp84eS7vhayYNyfYHtF8dqwRidN2xVHq1\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "outcome": "passed"}, "duration": 41.88961482048035, "name": "test.py::testOpenchannel[LndNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.39730358123779297, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [], \"method\": \"stop\"}\n", "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0021507740020751953, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 9, "call": {"name": "call", "duration": 18.88488221168518, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 032bfaaccc7024d9d02ae09bead75e38ccf39cfab0296cf0a13592864a49f0c71d@localhost:16331 -> 02cf25d73061fd408c1e2ecac474086ac6d65fd1addeae462d2c85cc0b971c56a2@localhost:16332\n", "outcome": "passed"}, "duration": 19.28761339187622, "name": "test.py::testConnect[LndNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.400954008102417, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0008885860443115234, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 19, "call": {"name": "call", "duration": 38.685314893722534, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2MztTg6TsTkmHBgAWFurysz7UH5AEciXNQq\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f06ac7a49d8>)", "outcome": "failed"}, "duration": 38.68946099281311, "name": "test.py::testOpenchannel[LndNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.0019025802612304688, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0011217594146728516, "outcome": "passed"}, "outcome": "failed"}, {"run_index": 10, "call": {"name": "call", "duration": 12.063075542449951, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 037721cd1410dacee0e8e757e5a8739b48294dd87c942c8c0126710011dac14fd0@localhost:16331 -> 03fcacd56fbc71a375166957ee8c146de4d7eb92bb0389f8cdb530ddd4d31bbc0c@localhost:16332\n", "outcome": "passed"}, "duration": 12.067589282989502, "name": "test.py::testConnect[LndNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.0006785392761230469, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0019176006317138672, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 13, "call": {"name": "call", "duration": 13.310176610946655, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"mnN8HPXoz3HQARBjQNCrLCW7bqCNuQMunK\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "outcome": "passed"}, "duration": 13.667227745056152, "name": "test.py::testOpenchannel[EclairNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.3518087863922119, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.002621173858642578, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 18, "call": {"name": "call", "duration": 44.89965224266052, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2NAeVUdacwwVyig5kMd6CBAS37dWoHgfQt1\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f06ac7987b8>)", "outcome": "failed"}, "duration": 45.30263423919678, "name": "test.py::testOpenchannel[LndNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.4017479419708252, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0006170272827148438, "outcome": "passed"}, "outcome": "failed"}, {"run_index": 14, "call": {"name": "call", "duration": 35.30176496505737, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"mrhc5nkxp6gt4yogbKP7iKet58mHszC4a3\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f06ad0291e0>)", "outcome": "failed"}, "duration": 35.7483766078949, "name": "test.py::testOpenchannel[EclairNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.4454514980316162, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0005800724029541016, "outcome": "passed"}, "outcome": "failed"}, {"run_index": 15, "call": {"name": "call", "duration": 17.18654227256775, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2NEHoLfTrjcTqykAgFchUFrBz6zV3oiGoMD\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [\"d1aa19c56e4e5b4f39937aec324c7d288e9e3daabc12d9c2be8e786aa61d0edb\"], \"method\": \"getrawtransaction\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "outcome": "passed"}, "duration": 17.634711742401123, "name": "test.py::testOpenchannel[LightningNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.44331860542297363, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0024254322052001953, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 3, "call": {"name": "call", "duration": 13.133764743804932, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 03981e490c8ebe37b3019610a1062749d4e82f736abe9fdf211de2fe18784f6166@localhost:16331 -> 0224afb97548dc0d64454f59f3008cc64632d49731b26530c861de865f1def11e5@localhost:16332\n", "outcome": "passed"}, "duration": 13.956263303756714, "name": "test.py::testConnect[EclairNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.8201754093170166, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0011615753173828125, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 2, "call": {"name": "call", "duration": 12.815724611282349, "outcome": "passed"}, "duration": 12.819088220596313, "name": "test.py::testStart[LndNode]", "teardown": {"name": "teardown", "duration": 0.0013117790222167969, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0010259151458740234, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 8, "call": {"name": "call", "duration": 10.84813117980957, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 02f45156664f257e743244355059092dc3c7c7cebc60d70d7b78a865cad61ad30f@localhost:16331 -> 02cec80c38814a3dca63bc717f345f26c06c8a3a6f459566be6aa9ff2794c27660@localhost:16332\n", "outcome": "passed"}, "duration": 10.851470470428467, "name": "test.py::testConnect[LightningNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.0008940696716308594, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0012226104736328125, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 16, "call": {"name": "call", "duration": 8.732974290847778, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [\"2N8pkfma8EVzigEqJesk4ijT4LyLazGJpGi\", 0.2], \"method\": \"sendtoaddress\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [\"f655174b0314cb8f2d475a44d4c4e360652cb78209bbdd421724ea7032cb2e4a\"], \"method\": \"getrawtransaction\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\n", "outcome": "passed"}, "duration": 8.737167119979858, "name": "test.py::testOpenchannel[LightningNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.001980304718017578, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.00110626220703125, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 1, "call": {"name": "call", "duration": 0.6247994899749756, "outcome": "passed"}, "duration": 0.6312432289123535, "name": "test.py::testStart[LightningNode]", "teardown": {"name": "teardown", "duration": 0.0012652873992919922, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0025892257690429688, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 4, "call": {"name": "call", "duration": 7.0577170848846436, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 03bc749285cdfcfba74a4a5b8e51fdff169eb25b0a0a8e8b2449e5e4e5f1014541@localhost:16331 -> 0381dd635a56ccb7b9b70edaa4b38ed78f15bfff4dcd3a7721521b0f54b1143791@localhost:16332\n", "outcome": "passed"}, "duration": 7.502035856246948, "name": "test.py::testConnect[EclairNode_LightningNode]", "teardown": {"name": "teardown", "duration": 0.44197750091552734, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0011706352233886719, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 11, "call": {"name": "call", "duration": 19.32174801826477, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 034b8489af7625967881cc3c88cc2b38c61c1db85022211434af024f45e3ba631a@localhost:16331 -> 0317cb109175e68fe7804fca3f33d857dad045a9bb21ae85269387f00d88c08982@localhost:16332\n", "outcome": "passed"}, "duration": 19.324888706207275, "name": "test.py::testConnect[LndNode_LndNode]", "teardown": {"name": "teardown", "duration": 0.0014526844024658203, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0008440017700195312, "outcome": "passed"}, "outcome": "passed"}, {"run_index": 6, "call": {"name": "call", "duration": 7.352493762969971, "stdout": "{\"id\": 1, \"version\": \"1.1\", \"params\": [1], \"method\": \"generate\"}\nConnecting 022ecdea47b89100ff580944cae25f515a5a40a4b4d7a72023ff12960a6d418d70@localhost:16331 -> 038589067c4a305c6ac463ca930e70a9d8f75cc509779dfaace0dd6eec73ab716b@localhost:16332\n", "outcome": "passed"}, "duration": 7.750866889953613, "name": "test.py::testConnect[LightningNode_EclairNode]", "teardown": {"name": "teardown", "duration": 0.3954901695251465, "outcome": "passed"}, "setup": {"name": "setup", "duration": 0.0014414787292480469, "outcome": "passed"}, "outcome": "passed"}], "created_at": "2017-08-16 15:06:27.139444", "versions": {"eclair": "d0a18c064993ea650a4ddcc6704ded08143d3100", "lightning": "f085a474b2d197d342ebe81a542c63f669eac78a", "lnd": "f59b505e01385a9c3efc7e3f71b644419b628228"}, "summary": {"duration": 369.21935391426086, "passed": 17, "failed": 4, "num_tests": 21}, "environment": {"Platform": "Linux-4.4.0-75-generic-x86_64-with-Ubuntu-16.04-xenial", "Python": "3.5.2"}} -------------------------------------------------------------------------------- /reports/ece062dbbb45b913af4c287b4c3a77d46bd722f10c7771eb74516249e113c360.json: -------------------------------------------------------------------------------- 1 | {"summary": {"failed": 5, "duration": 260.60087847709656, "num_tests": 21, "passed": 16}, "created_at": "2017-08-11 12:42:42.070174", "environment": {"Platform": "Linux-4.4.0-75-generic-x86_64-with-Ubuntu-16.04-xenial", "Python": "3.5.2"}, "tests": [{"setup": {"outcome": "passed", "duration": 0.0010919570922851562, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.39534592628479004, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [\"2N4Z9Foa9o5xTooS3BgQzFdPjwTag6YQDiY\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [\"f32bb43c29d323881b463194c18218cc58cb544e6699bf9efdf144a1fcd83d5d\"], \"method\": \"getrawtransaction\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 16.959789991378784, "name": "call"}, "run_index": 15, "duration": 17.357319831848145, "name": "test.py::testOpenchannel[LightningNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0009980201721191406, "name": "setup"}, "outcome": "failed", "teardown": {"outcome": "passed", "duration": 0.37857770919799805, "name": "teardown"}, "call": {"outcome": "failed", "duration": 14.641435384750366, "name": "call", "longrepr": "test.py:151: in testOpenchannel\n node1.addfunds(bitcoind, 2 * 10**7)\nE AttributeError: 'LndNode' object has no attribute 'addfunds'"}, "run_index": 18, "duration": 15.022009134292603, "name": "test.py::testOpenchannel[LndNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0006761550903320312, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.7843847274780273, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 02a898eb26c974d38981e5b4365d87f3c4f038b50d38f1a0bc9a078a3ef180ff02@localhost:16331 -> 02f02ecb841b148a1cf1d13c18ed7151adc5cbff367ff7674ff8bf8245e6c2d18c@localhost:16332\n", "duration": 9.767205476760864, "name": "call"}, "run_index": 3, "duration": 10.552942514419556, "name": "test.py::testConnect[EclairNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0029189586639404297, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.3970675468444824, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 02af8edc28049196b8098c582a35276bad82dba197f156095a73ff283d02612a48@localhost:16331 -> 03ad869198a760d3f601bf7574ef70907f1ce03a721c64d844f5e09f188f5f17b7@localhost:16332\n", "duration": 14.050551414489746, "name": "call"}, "run_index": 9, "duration": 14.45345687866211, "name": "test.py::testConnect[LndNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0025568008422851562, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.8370938301086426, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [\"mvgzLFSmMibjonnevf2Fev6wKSH43GmkGq\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 16.48633337020874, "name": "call"}, "run_index": 12, "duration": 17.328540802001953, "name": "test.py::testOpenchannel[EclairNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0006239414215087891, "name": "setup"}, "outcome": "failed", "teardown": {"outcome": "passed", "duration": 0.0009872913360595703, "name": "teardown"}, "call": {"outcome": "failed", "duration": 10.505929708480835, "name": "call", "longrepr": "test.py:151: in testOpenchannel\n node1.addfunds(bitcoind, 2 * 10**7)\nE AttributeError: 'LndNode' object has no attribute 'addfunds'"}, "run_index": 19, "duration": 10.508164882659912, "name": "test.py::testOpenchannel[LndNode_LightningNode]"}, {"setup": {"outcome": "passed", "duration": 0.0006368160247802734, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.0005528926849365234, "name": "teardown"}, "call": {"outcome": "passed", "duration": 0.5572433471679688, "name": "call"}, "run_index": 1, "duration": 0.5590698719024658, "name": "test.py::testStart[LightningNode]"}, {"setup": {"outcome": "passed", "duration": 0.0009407997131347656, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.001909494400024414, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 034ad212ecd6866eb4ed6e4a4d1494b81edf0f2905b8d3bc2cce34f8f5b6923a14@localhost:16331 -> 03f7557d1707056ce70e428f9aadccb2bdfd80b10915ae2266e1778ef6bb4349bb@localhost:16332\n", "duration": 1.1469850540161133, "name": "call"}, "run_index": 7, "duration": 1.1507761478424072, "name": "test.py::testConnect[LightningNode_LightningNode]"}, {"setup": {"outcome": "passed", "duration": 0.0013957023620605469, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.39853763580322266, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 03b493844e5b400512902e2e7ff00d70d7f8fe6896a774241fbec96e0c17e12dc4@localhost:16331 -> 0220dfc693d97e654382b005c68ffeebbcc48060a2681bf8363e971f89e2e4df4f@localhost:16332\n", "duration": 15.506738662719727, "name": "call"}, "run_index": 5, "duration": 15.90806770324707, "name": "test.py::testConnect[EclairNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0011172294616699219, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.40659093856811523, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [\"mv7hNv6rLt9pNpEG8HRox1i4rCX1vf8Hom\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 13.103227138519287, "name": "call"}, "run_index": 13, "duration": 13.512052536010742, "name": "test.py::testOpenchannel[EclairNode_LightningNode]"}, {"setup": {"outcome": "passed", "duration": 0.0021598339080810547, "name": "setup"}, "outcome": "failed", "teardown": {"outcome": "passed", "duration": 0.39025115966796875, "name": "teardown"}, "call": {"outcome": "failed", "stdout": "{\"version\": \"1.1\", \"params\": [\"mjJ8x52DzNFE8TXPBhyA1H3GnxYBEinhxR\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 32.75830388069153, "name": "call", "longrepr": "test.py:160: in testOpenchannel\n wait_for(lambda: node1.check_channel(node2), interval=1, timeout=10)\ntest.py:107: in wait_for\n raise ValueError(\"Error waiting for {}\", success)\nE ValueError: ('Error waiting for {}', . at 0x7f98c83cc7b8>)"}, "run_index": 14, "duration": 33.15287470817566, "name": "test.py::testOpenchannel[EclairNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0007760524749755859, "name": "setup"}, "outcome": "failed", "teardown": {"outcome": "passed", "duration": 0.001963376998901367, "name": "teardown"}, "call": {"outcome": "failed", "stdout": "{\"version\": \"1.1\", \"params\": [\"2N9XKEoRTjGufzZrFP2HJFg2pCveFxStdVP\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [\"d5732d34ec731e883d0d81f15591548d5afb2d6a09690e9c65681acde0e4bc24\"], \"method\": \"getrawtransaction\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 11.841633558273315, "name": "call", "longrepr": "test.py:155: in testOpenchannel\n node1.openchannel(node2.id(), 'localhost', node2.daemon.port, 10**7)\nlightningd.py:69: in openchannel\n return self.rpc.fundchannel(node_id, satoshis)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:44: in wrapper\n return self._call(name, args)\n.direnv/python-3.5.2/lib/python3.5/site-packages/lightning/lightning.py:62: in _call\n raise ValueError(\"RPC call failed: {}\".format(resp['error']))\nE ValueError: RPC call failed: Peer died"}, "run_index": 17, "duration": 11.845149040222168, "name": "test.py::testOpenchannel[LightningNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0007989406585693359, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.0020961761474609375, "name": "teardown"}, "call": {"outcome": "passed", "duration": 9.733470678329468, "name": "call"}, "run_index": 2, "duration": 9.737164735794067, "name": "test.py::testStart[LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.001161813735961914, "name": "setup"}, "outcome": "failed", "teardown": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [], \"method\": \"stop\", \"id\": 1}\n", "duration": 0.3760523796081543, "name": "teardown"}, "call": {"outcome": "failed", "duration": 18.81413459777832, "name": "call", "longrepr": "test.py:151: in testOpenchannel\n node1.addfunds(bitcoind, 2 * 10**7)\nE AttributeError: 'LndNode' object has no attribute 'addfunds'"}, "run_index": 20, "duration": 19.1925106048584, "name": "test.py::testOpenchannel[LndNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0012392997741699219, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.41386866569519043, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 029ebcf8e7c00acba14d98bd41e9e5feb8b41ee4bc8e4a9f4f1e4d48b48e06172c@localhost:16331 -> 03ddd43d56973a4f4300fdd37ee3ea030952578f928e41e30849affb8c4b1d5454@localhost:16332\n", "duration": 6.399290084838867, "name": "call"}, "run_index": 4, "duration": 6.8156373500823975, "name": "test.py::testConnect[EclairNode_LightningNode]"}, {"setup": {"outcome": "passed", "duration": 0.002769947052001953, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.002033233642578125, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 03fe3074187815d38a1e7e917bd5c6da5f8e38df415ac062879d4522c620ecd6f5@localhost:16331 -> 02c9b12d6f0741860daecd6e682df9ab99d95e8cacd70a663d53e4f6bc63beea59@localhost:16332\n", "duration": 18.511323928833008, "name": "call"}, "run_index": 11, "duration": 18.51889705657959, "name": "test.py::testConnect[LndNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0019626617431640625, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.002697467803955078, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 02ea187c5f24b486d49aac7179b1a808840acaefd08d114aafc74831249bbf1cd1@localhost:16331 -> 0246111282409a03570e5d45d433813c736f7b90d4a5169aef9facff92fa68c7b7@localhost:16332\n", "duration": 9.708709478378296, "name": "call"}, "run_index": 8, "duration": 9.715332269668579, "name": "test.py::testConnect[LightningNode_LndNode]"}, {"setup": {"outcome": "passed", "duration": 0.0025184154510498047, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.0008194446563720703, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [\"2N5bLcFJcA2Msd8GS5LT57riw7qEcKJAnta\", 0.2], \"method\": \"sendtoaddress\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [\"568923acb7ee59d5c45367218c0c52b430b65d05f1aeb7ff318548b42486de1f\"], \"method\": \"getrawtransaction\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\n", "duration": 9.067672729492188, "name": "call"}, "run_index": 16, "duration": 9.07352900505066, "name": "test.py::testOpenchannel[LightningNode_LightningNode]"}, {"setup": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [], \"method\": \"getinfo\", \"id\": 1}\n", "duration": 1.7037551403045654, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.39668965339660645, "name": "teardown"}, "call": {"outcome": "passed", "duration": 7.5770978927612305, "name": "call"}, "run_index": 0, "duration": 11.381297826766968, "name": "test.py::testStart[EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.003737926483154297, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.40412378311157227, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 02bd7bb03538f5084067b524f3779abbeb9cdf789d47ba61d4c439fe3be8f2f27c@localhost:16331 -> 03470df0a1e11a7acf8bd9cfd1554039761ace5382d1a609c803d40d784a76a7d3@localhost:16332\n", "duration": 5.900038957595825, "name": "call"}, "run_index": 6, "duration": 6.311638593673706, "name": "test.py::testConnect[LightningNode_EclairNode]"}, {"setup": {"outcome": "passed", "duration": 0.0019392967224121094, "name": "setup"}, "outcome": "passed", "teardown": {"outcome": "passed", "duration": 0.0017170906066894531, "name": "teardown"}, "call": {"outcome": "passed", "stdout": "{\"version\": \"1.1\", \"params\": [1], \"method\": \"generate\", \"id\": 1}\nConnecting 03a6baf779344be12aab0bbd36467015cff156b3b68ee32d847a172b60d019be73@localhost:16331 -> 026136ac97f541b5a6260f6dd6a2ead2484487624a9f12a6a4e8ad39e58c12ae86@localhost:16332\n", "duration": 9.789081573486328, "name": "call"}, "run_index": 10, "duration": 9.794677257537842, "name": "test.py::testConnect[LndNode_LightningNode]"}], "versions": {"eclair": "d0a18c064993ea650a4ddcc6704ded08143d3100", "lightning": "fc59d8e227ca6a4b30902698a9355c5aebddba6e", "lnd": "1e85fa9c5f06aef5f887304378c660e37768ab8b"}, "id": "ece062dbbb45b913af4c287b4c3a77d46bd722f10c7771eb74516249e113c360"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-bitcoinlib==0.10.1 2 | psutil==5.4.8 3 | pytest==4.0.2 4 | requests==2.21.0 5 | pylightning==0.0.6 6 | pytest-json==0.4.0 7 | grpcio==1.17.1 8 | bitstring==3.1.5 9 | base58==1.0.2 10 | secp256k1==0.13.2 11 | google-cloud-storage==1.13.2 12 | click==7.0 13 | staticjinja==0.3.5 14 | pytest-rerunfailures==5.0 15 | pytest-timeout==1.3.3 16 | flask==1.0.2 17 | ephemeral-port-reserve==1.1.0 18 | CherryPy==18.1.0 19 | pytest-xdist==1.25.0 20 | -------------------------------------------------------------------------------- /templates/_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Lightning Integration Tests 5 | 6 | 7 | 8 | 43 | 45 | 46 | 47 |
48 |

{% block subtitle %}{% endblock %}

49 | {% block content %}{% endblock %} 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /templates/_report.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | 3 | {% block subtitle %} 4 | Test Run {{ id[:7] }} 5 | 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 | « Back to overview 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for k, v in versions|dictsort %} 23 | 24 | 25 | 26 | {% endfor %} 27 | 28 |
ClientCommit
{{ k }}{{ v }}
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for name, test in tests|dictsort %} 41 | 42 | 43 | 44 | 45 | 46 | {% for stest in test.subtests %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% endfor %} 54 | {% endfor %} 55 | 56 |
TestConfigurationResult
{{ name }} {{ test.success }}/{{ test.total }}
 {{ stest.name.replace("_", " ") }}{{ stest.call.outcome }}
57 | 58 | 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | 3 | {% block subtitle %} 4 | Lightning Integration Tests 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |

11 | This page shows the latest integration test results for four Lightning implementations. 12 | We test the latest versions of: 13 |

19 | against each other in a number of scenarios and see if they can cooperate. 20 |

21 |

22 | Below is a table of the latest test runs, with their respective scores. 23 | Click on the ID link to see the detailed test results for that run. 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% for report in reports %} 36 | 37 | 38 | 39 | 44 | 45 | {% endfor %} 46 | 47 |
IDDate/TimeResult
{{ report.id[:7] }}{{ report.created_at[:19] }} 40 | 41 | {{ report.summary.passed }} / {{report.summary.num_tests }} 42 | 43 |
48 |

49 | Notice that the results are still not stable and compatibility may be 50 | broken from time to time as implementations move closer to being spec 51 | compliant, or as we add new scenarios. 52 |

53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from binascii import unhexlify, hexlify 2 | from btcproxy import ProxiedBitcoinD 3 | from eclair import EclairNode 4 | from ephemeral_port_reserve import reserve 5 | from hashlib import sha256 6 | from itertools import product 7 | from lightningd import LightningNode 8 | from lnaddr import lndecode 9 | from lnd import LndNode 10 | from ptarmd import PtarmNode 11 | from concurrent import futures 12 | from utils import BitcoinD, BtcD 13 | from bech32 import bech32_decode 14 | 15 | from fixtures import * 16 | 17 | import logging 18 | import os 19 | import pytest 20 | import sys 21 | import tempfile 22 | import time 23 | 24 | impls = [EclairNode, LightningNode, LndNode, PtarmNode] 25 | 26 | if TEST_DEBUG: 27 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 28 | logging.info("Tests running in '%s'", TEST_DIR) 29 | 30 | 31 | def transact_and_mine(btc): 32 | """ Generate some transactions and blocks. 33 | 34 | To make bitcoind's `estimatesmartfee` succeeded. 35 | """ 36 | addr = btc.rpc.getnewaddress() 37 | for i in range(10): 38 | for j in range(10): 39 | txid = btc.rpc.sendtoaddress(addr, 0.5) 40 | btc.rpc.generatetoaddress(1, addr) 41 | 42 | 43 | def wait_for(success, timeout=30, interval=1): 44 | start_time = time.time() 45 | while not success() and time.time() < start_time + timeout: 46 | time.sleep(interval) 47 | if time.time() > start_time + timeout: 48 | raise ValueError("Error waiting for {}", success) 49 | 50 | 51 | def sync_blockheight(btc, nodes): 52 | info = btc.rpc.getblockchaininfo() 53 | blocks = info['blocks'] 54 | 55 | print("Waiting for %d nodes to blockheight %d" % (len(nodes), blocks)) 56 | for n in nodes: 57 | wait_for(lambda: n.info()['blockheight'] == blocks, interval=1) 58 | 59 | 60 | def generate_until(btc, success, blocks=30, interval=1): 61 | """Generate new blocks until `success` returns true. 62 | 63 | Mainly used to wait for transactions to confirm since they might 64 | be delayed and we don't want to add a long waiting time to all 65 | tests just because some are slow. 66 | """ 67 | addr = btc.rpc.getnewaddress() 68 | for i in range(blocks): 69 | time.sleep(interval) 70 | if success(): 71 | return 72 | btc.rpc.generatetoaddress(1, addr) 73 | time.sleep(interval) 74 | if not success(): 75 | raise ValueError("Generated %d blocks, but still no success", blocks) 76 | 77 | 78 | def idfn(impls): 79 | return "_".join([i.displayName for i in impls]) 80 | 81 | 82 | @pytest.mark.parametrize("impl", impls, ids=idfn) 83 | def test_start(bitcoind, node_factory, impl): 84 | node = node_factory.get_node(implementation=impl) 85 | assert node.ping() 86 | sync_blockheight(bitcoind, [node]) 87 | 88 | 89 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 90 | def test_connect(node_factory, bitcoind, impls): 91 | node1 = node_factory.get_node(implementation=impls[0]) 92 | node2 = node_factory.get_node(implementation=impls[1]) 93 | 94 | # Needed by lnd in order to have at least one block in the last 2 hours 95 | addr = bitcoind.rpc.getnewaddress() 96 | bitcoind.rpc.generatetoaddress(1, addr) 97 | 98 | print("Connecting {}@{}:{} -> {}@{}:{}".format( 99 | node1.id(), 'localhost', node1.daemon.port, 100 | node2.id(), 'localhost', node2.daemon.port)) 101 | node1.connect('localhost', node2.daemon.port, node2.id()) 102 | 103 | wait_for(lambda: node1.peers(), timeout=5) 104 | wait_for(lambda: node2.peers(), timeout=5) 105 | 106 | # TODO(cdecker) Check that we are connected 107 | assert node1.id() in node2.peers() 108 | assert node2.id() in node1.peers() 109 | 110 | 111 | def confirm_channel(bitcoind, n1, n2): 112 | print("Waiting for channel {} -> {} to confirm".format(n1.id(), n2.id())) 113 | assert n1.id() in n2.peers() 114 | assert n2.id() in n1.peers() 115 | addr = bitcoind.rpc.getnewaddress() 116 | for i in range(10): 117 | time.sleep(2) 118 | if n1.check_channel(n2) and n2.check_channel(n1): 119 | print("Channel {} -> {} confirmed".format(n1.id(), n2.id())) 120 | return True 121 | bhash = bitcoind.rpc.generatetoaddress(1, addr)[0] 122 | n1.block_sync(bhash) 123 | n2.block_sync(bhash) 124 | 125 | # Last ditch attempt 126 | return n1.check_channel(n2) and n2.check_channel(n1) 127 | 128 | 129 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 130 | def test_open_channel(bitcoind, node_factory, impls): 131 | node1 = node_factory.get_node(implementation=impls[0]) 132 | node2 = node_factory.get_node(implementation=impls[1]) 133 | 134 | node1.connect('localhost', node2.daemon.port, node2.id()) 135 | 136 | wait_for(lambda: node1.peers(), interval=1) 137 | wait_for(lambda: node2.peers(), interval=1) 138 | 139 | node1.addfunds(bitcoind, 2 * 10**7) 140 | 141 | node1.openchannel(node2.id(), 'localhost', node2.daemon.port, 10**7) 142 | addr = bitcoind.rpc.getnewaddress() 143 | time.sleep(1) 144 | bitcoind.rpc.generatetoaddress(2, addr) 145 | 146 | assert confirm_channel(bitcoind, node1, node2) 147 | 148 | assert(node1.check_channel(node2)) 149 | assert(node2.check_channel(node1)) 150 | 151 | # Generate some more, to reach the announcement depth 152 | bitcoind.rpc.generatetoaddress(4, addr) 153 | 154 | 155 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 156 | def test_gossip(node_factory, bitcoind, impls): 157 | """ Create a network of lightningd nodes and connect to it using 2 new nodes 158 | """ 159 | # These are the nodes we really want to test 160 | node1 = node_factory.get_node(implementation=impls[0]) 161 | node2 = node_factory.get_node(implementation=impls[1]) 162 | 163 | # Using lightningd since it is quickest to start up 164 | nodes = [node_factory.get_node(implementation=LightningNode) for _ in range(5)] 165 | for n1, n2 in zip(nodes[:4], nodes[1:]): 166 | n1.connect('localhost', n2.daemon.port, n2.id()) 167 | n1.addfunds(bitcoind, 2 * 10**7) 168 | n1.openchannel(n2.id(), 'localhost', n2.daemon.port, 10**7) 169 | assert confirm_channel(bitcoind, n1, n2) 170 | 171 | time.sleep(5) 172 | addr = bitcoind.rpc.getnewaddress() 173 | bitcoind.rpc.generatetoaddress(30, addr) 174 | time.sleep(5) 175 | 176 | # Wait for gossip to settle 177 | for n in nodes: 178 | wait_for(lambda: len(n.getnodes()) == 5, interval=1, timeout=120) 179 | wait_for(lambda: len(n.getchannels()) == 8, interval=1, timeout=120) 180 | 181 | # Now connect the first node to the line graph and the second one to the first 182 | node1.connect('localhost', nodes[0].daemon.port, nodes[0].id()) 183 | node2.connect('localhost', n1.daemon.port, n1.id()) 184 | 185 | # They should now be syncing as well 186 | # TODO(cdecker) Uncomment the following line when eclair exposes non-local channels as well (ACINQ/eclair/issues/126) 187 | #wait_for(lambda: len(node1.getchannels()) == 8) 188 | wait_for(lambda: len(node1.getnodes()) == 5, interval=1) 189 | 190 | # Node 2 syncs through node 1 191 | # TODO(cdecker) Uncomment the following line when eclair exposes non-local channels as well (ACINQ/eclair/issues/126) 192 | #wait_for(lambda: len(node2.getchannels()) == 8) 193 | wait_for(lambda: len(node2.getnodes()) == 5, interval=1) 194 | 195 | 196 | @pytest.mark.parametrize("impl", impls, ids=idfn) 197 | def test_invoice_decode(node_factory, impl): 198 | capacity = 10**7 199 | node1 = node_factory.get_node(implementation=impl) 200 | 201 | amount = int(capacity / 10) 202 | payment_request = node1.invoice(amount) 203 | hrp, data = bech32_decode(payment_request) 204 | 205 | assert hrp and data 206 | assert hrp.startswith('lnbcrt') 207 | 208 | 209 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 210 | def test_direct_payment(bitcoind, node_factory, impls): 211 | node1 = node_factory.get_node(implementation=impls[0]) 212 | node2 = node_factory.get_node(implementation=impls[1]) 213 | capacity = 10**7 214 | 215 | node1.connect('localhost', node2.daemon.port, node2.id()) 216 | 217 | wait_for(lambda: node1.peers(), interval=1) 218 | wait_for(lambda: node2.peers(), interval=1) 219 | 220 | node1.addfunds(bitcoind, 2*capacity) 221 | time.sleep(5) 222 | addr = bitcoind.rpc.getnewaddress() 223 | bitcoind.rpc.generatetoaddress(10, addr) 224 | time.sleep(5) 225 | 226 | node1.openchannel(node2.id(), 'localhost', node2.daemon.port, capacity) 227 | assert confirm_channel(bitcoind, node1, node2) 228 | 229 | sync_blockheight(bitcoind, [node1, node2]) 230 | 231 | amount = int(capacity / 10) 232 | req = node2.invoice(amount) 233 | dec = lndecode(req) 234 | 235 | print("Decoded payment request", req, dec) 236 | payment_key = node1.send(req) 237 | assert(sha256(unhexlify(payment_key)).digest() == dec.paymenthash) 238 | 239 | 240 | def gossip_is_synced(nodes, num_channels): 241 | print("Checking %d nodes for gossip sync" % (len(nodes))) 242 | for i, n in enumerate(nodes): 243 | node_chans = n.getchannels() 244 | logging.debug("Node {} knows about the following channels {}".format(i, node_chans)) 245 | if len(node_chans) != num_channels: 246 | print("Node %d is missing %d channels" % (i, num_channels - len(node_chans))) 247 | return False 248 | return True 249 | 250 | 251 | def check_channels(pairs): 252 | ok = True 253 | logging.debug("Checking all channels between {}".format(pairs)) 254 | for node1, node2 in pairs: 255 | ok &= node1.check_channel(node2) 256 | ok &= node2.check_channel(node1) 257 | return ok 258 | 259 | 260 | def node_has_route(node, channels): 261 | """Check whether a node knows about a specific route. 262 | 263 | The route is a list of node_id tuples 264 | """ 265 | return set(channels).issubset(set(node.getchannels())) 266 | 267 | 268 | @pytest.mark.parametrize("impls", product(impls, repeat=3), ids=idfn) 269 | def test_forwarded_payment(bitcoind, node_factory, impls): 270 | num_nodes = len(impls) 271 | nodes = [node_factory.get_node(implementation=impls[i]) for i in range(3)] 272 | capacity = 10**7 273 | 274 | for i in range(num_nodes-1): 275 | nodes[i].connect('localhost', nodes[i+1].daemon.port, nodes[i+1].id()) 276 | nodes[i].addfunds(bitcoind, 4 * capacity) 277 | 278 | for i in range(num_nodes-1): 279 | nodes[i].openchannel(nodes[i+1].id(), 'localhost', nodes[i+1].daemon.port, capacity) 280 | assert confirm_channel(bitcoind, nodes[i], nodes[i+1]) 281 | 282 | addr = bitcoind.rpc.getnewaddress() 283 | bitcoind.rpc.generatetoaddress(6, addr) 284 | sync_blockheight(bitcoind, nodes) 285 | 286 | # Make sure we have a path 287 | ids = [n.info()['id'] for n in nodes] 288 | route = [(ids[i-1], ids[i]) for i in range(1, len(ids))] 289 | wait_for(lambda: node_has_route(nodes[0], route), timeout=120) 290 | sync_blockheight(bitcoind, nodes) 291 | 292 | src = nodes[0] 293 | dst = nodes[len(nodes)-1] 294 | amount = int(capacity / 10) 295 | req = dst.invoice(amount) 296 | 297 | print("Waiting for a route to be found") 298 | wait_for(lambda: src.check_route(dst.id(), amount), timeout=120) 299 | 300 | payment_key = src.send(req) 301 | dec = lndecode(req) 302 | assert(sha256(unhexlify(payment_key)).digest() == dec.paymenthash) 303 | 304 | 305 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 306 | def test_reconnect(bitcoind, node_factory, impls): 307 | node1 = node_factory.get_node(implementation=impls[0]) 308 | node2 = node_factory.get_node(implementation=impls[1]) 309 | capacity = 10**7 310 | 311 | node1.connect('localhost', node2.daemon.port, node2.id()) 312 | 313 | wait_for(lambda: node1.peers(), interval=1) 314 | wait_for(lambda: node2.peers(), interval=1) 315 | 316 | node1.addfunds(bitcoind, 2*capacity) 317 | time.sleep(5) 318 | addr = bitcoind.rpc.getnewaddress() 319 | bitcoind.rpc.generatetoaddress(10, addr) 320 | time.sleep(5) 321 | 322 | node1.openchannel(node2.id(), 'localhost', node2.daemon.port, capacity) 323 | 324 | addr = bitcoind.rpc.getnewaddress() 325 | for i in range(30): 326 | node1.bitcoin.rpc.generatetoaddress(1, addr) 327 | time.sleep(1) 328 | 329 | wait_for(lambda: node1.check_channel(node2)) 330 | wait_for(lambda: node2.check_channel(node1)) 331 | sync_blockheight(bitcoind, [node1, node2]) 332 | 333 | amount = int(capacity / 10) 334 | req = node2.invoice(amount) 335 | payment_key = node1.send(req) 336 | dec = lndecode(req) 337 | assert(sha256(unhexlify(payment_key)).digest() == dec.paymenthash) 338 | 339 | print("Sleep before restart") 340 | time.sleep(5) 341 | 342 | print("Restarting") 343 | node2.restart() 344 | 345 | time.sleep(15) 346 | 347 | wait_for(lambda: node1.check_channel(node2)) 348 | wait_for(lambda: node2.check_channel(node1)) 349 | sync_blockheight(bitcoind, [node1, node2]) 350 | 351 | time.sleep(15) 352 | 353 | req = node2.invoice(amount) 354 | payment_key = node1.send(req) 355 | dec = lndecode(req) 356 | assert(sha256(unhexlify(payment_key)).digest() == dec.paymenthash) 357 | 358 | 359 | @pytest.mark.parametrize("impls", product(impls, repeat=2), ids=idfn) 360 | def test_reconnect_across_channel_open(bitcoind, node_factory, impls): 361 | node1 = node_factory.get_node(implementation=impls[0]) 362 | node2 = node_factory.get_node(implementation=impls[1]) 363 | capacity = 10**7 364 | 365 | node1.connect('localhost', node2.daemon.port, node2.id()) 366 | 367 | wait_for(lambda: node1.peers(), interval=1) 368 | wait_for(lambda: node2.peers(), interval=1) 369 | 370 | node1.addfunds(bitcoind, 2*capacity) 371 | addr = bitcoind.rpc.getnewaddress() 372 | time.sleep(5) 373 | bitcoind.rpc.generatetoaddress(10, addr) 374 | time.sleep(5) 375 | 376 | node1.openchannel(node2.id(), 'localhost', node2.daemon.port, capacity) 377 | 378 | for i in range(5): 379 | node1.bitcoin.rpc.generatetoaddress(1, addr) 380 | time.sleep(1) 381 | 382 | node1.stop() 383 | 384 | for i in range(25): 385 | node1.bitcoin.rpc.generatetoaddress(1, addr) 386 | time.sleep(1) 387 | 388 | node1.start() 389 | wait_for(lambda: node1.check_channel(node2), timeout=120) 390 | wait_for(lambda: node2.check_channel(node1), timeout=120) 391 | sync_blockheight(bitcoind, [node1, node2]) 392 | -------------------------------------------------------------------------------- /tls.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBijCCATGgAwIBAgIJAKD6zs0dG8vpMAoGCCqGSM49BAMCMCIxEjAQBgNVBAMM 3 | CWxvY2FsaG9zdDEMMAoGA1UECgwDbG5kMB4XDTE3MDgwODEwMTc1MFoXDTI3MDgw 4 | NjEwMTc1MFowIjESMBAGA1UEAwwJbG9jYWxob3N0MQwwCgYDVQQKDANsbmQwWTAT 5 | BgcqhkjOPQIBBggqhkjOPQMBBwNCAASp6oi2+jfoyqfhHX8D16gMrBwj0lTDBE7f 6 | qPD7mke/XA8tGW5+x/ytRuRP4e0i3PIyNn3NiNgB01gAIBsxeVJzo1AwTjAdBgNV 7 | HQ4EFgQUXBJTCOjNOpAcpf5pU3FNcnrNpNIwHwYDVR0jBBgwFoAUXBJTCOjNOpAc 8 | pf5pU3FNcnrNpNIwDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiAb99q1 9 | dk/8l3QjXAkms9hfXvBKl2L5sZqQCrSSWmCSrQIgQGieaNxTVZULhV+6su8oXiHu 10 | C2bi5CebxLPph+Pb2aY= 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /tls.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIExc3wMePmf9ECeu//VFst3MXV+4m05bsBDaWnDquflwoAoGCCqGSM49 6 | AwEHoUQDQgAEqeqItvo36Mqn4R1/A9eoDKwcI9JUwwRO36jw+5pHv1wPLRlufsf8 7 | rUbkT+HtItzyMjZ9zYjYAdNYACAbMXlScw== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from bitcoin.rpc import RawProxy as BitcoinProxy 2 | from ephemeral_port_reserve import reserve 3 | 4 | import logging 5 | import re 6 | import subprocess 7 | import threading 8 | import time 9 | import os 10 | import collections 11 | import json 12 | import base64 13 | import requests 14 | 15 | 16 | BITCOIND_CONFIG = collections.OrderedDict([ 17 | ("server", 1), 18 | ("txindex", 1), 19 | ("deprecatedrpc", "addwitnessaddress"), 20 | ("addresstype", "p2sh-segwit"), 21 | ("deprecatedrpc", "signrawtransaction"), 22 | ("rpcuser", "rpcuser"), 23 | ("rpcpassword", "rpcpass"), 24 | ("listen", 0), 25 | ]) 26 | 27 | 28 | def write_config(filename, opts): 29 | with open(filename, 'w') as f: 30 | write_dict(f, opts) 31 | 32 | def write_dict(f, opts): 33 | for k, v in opts.items(): 34 | if isinstance(v, dict): 35 | f.write("[{}]\n".format(k)) 36 | write_dict(f, v) 37 | else: 38 | f.write("{}={}\n".format(k, v)) 39 | 40 | 41 | class TailableProc(object): 42 | """A monitorable process that we can start, stop and tail. 43 | 44 | This is the base class for the daemons. It allows us to directly 45 | tail the processes and react to their output. 46 | """ 47 | 48 | def __init__(self, outputDir=None, prefix='proc'): 49 | self.logs = [] 50 | self.logs_cond = threading.Condition(threading.RLock()) 51 | self.cmd_line = None 52 | self.running = False 53 | self.proc = None 54 | self.outputDir = outputDir 55 | self.logger = logging.getLogger(prefix) 56 | 57 | def start(self): 58 | """Start the underlying process and start monitoring it. 59 | """ 60 | self.thread = threading.Thread(target=self.tail) 61 | self.thread.daemon = True 62 | logging.debug("Starting '%s'", " ".join(self.cmd_line)) 63 | self.proc = subprocess.Popen(self.cmd_line, stdout=subprocess.PIPE) 64 | self.thread.start() 65 | self.running = True 66 | 67 | def save_log(self): 68 | if self.outputDir: 69 | logpath = os.path.join(self.outputDir, 'log.' + str(int(time.time()))) 70 | with open(logpath, 'w') as f: 71 | for l in self.logs: 72 | f.write(l + '\n') 73 | 74 | def stop(self): 75 | self.proc.terminate() 76 | self.proc.kill() 77 | self.save_log() 78 | 79 | def tail(self): 80 | """Tail the stdout of the process and remember it. 81 | 82 | Stores the lines of output produced by the process in 83 | self.logs and signals that a new line was read so that it can 84 | be picked up by consumers. 85 | """ 86 | for line in iter(self.proc.stdout.readline, ''): 87 | if len(line) == 0: 88 | break 89 | with self.logs_cond: 90 | self.logs.append(str(line.rstrip())) 91 | self.logger.debug(line.decode().rstrip()) 92 | self.logs_cond.notifyAll() 93 | self.running = False 94 | 95 | def is_in_log(self, regex): 96 | """Look for `regex` in the logs.""" 97 | 98 | ex = re.compile(regex) 99 | for l in self.logs: 100 | if ex.search(l): 101 | logging.debug("Found '%s' in logs", regex) 102 | return True 103 | 104 | logging.debug("Did not find '%s' in logs", regex) 105 | return False 106 | 107 | def wait_for_log(self, regex, offset=1000, timeout=60): 108 | """Look for `regex` in the logs. 109 | 110 | We tail the stdout of the process and look for `regex`, 111 | starting from `offset` lines in the past. We fail if the 112 | timeout is exceeded or if the underlying process exits before 113 | the `regex` was found. The reason we start `offset` lines in 114 | the past is so that we can issue a command and not miss its 115 | effects. 116 | 117 | """ 118 | logging.debug("Waiting for '%s' in the logs", regex) 119 | ex = re.compile(regex) 120 | start_time = time.time() 121 | pos = max(len(self.logs) - offset, 0) 122 | initial_pos = len(self.logs) 123 | while True: 124 | if time.time() > start_time + timeout: 125 | print("Can't find {} in logs".format(regex)) 126 | with self.logs_cond: 127 | for i in range(initial_pos, len(self.logs)): 128 | print(" " + self.logs[i]) 129 | if self.is_in_log(regex): 130 | print("(Was previously in logs!") 131 | raise TimeoutError( 132 | 'Unable to find "{}" in logs.'.format(regex)) 133 | elif not self.running: 134 | print('Logs: {}'.format(self.logs)) 135 | raise ValueError('Process died while waiting for logs') 136 | 137 | with self.logs_cond: 138 | if pos >= len(self.logs): 139 | self.logs_cond.wait(1) 140 | continue 141 | 142 | if ex.search(self.logs[pos]): 143 | logging.debug("Found '%s' in logs", regex) 144 | return self.logs[pos] 145 | pos += 1 146 | 147 | 148 | class BitcoinRpc(object): 149 | def __init__(self, url=None, rpcport=8332, rpcuser=None, rpcpassword=None): 150 | self.url = url if url else "http://localhost:{}".format(rpcport) 151 | authpair = "%s:%s" % (rpcuser, rpcpassword) 152 | authpair = authpair.encode('utf8') 153 | self.auth_header = b"Basic " + base64.b64encode(authpair) 154 | self.__id_count = 0 155 | 156 | def _call(self, service_name, *args): 157 | self.__id_count += 1 158 | 159 | r = requests.post(self.url, 160 | data=json.dumps({ 161 | 'version': '1.1', 162 | 'method': service_name, 163 | 'params': args, 164 | 'id': self.__id_count}), 165 | headers={ 166 | # 'Host': self.__url.hostname, 167 | 'Authorization': self.auth_header, 168 | 'Content-type': 'application/json' 169 | }) 170 | 171 | response = r.json() 172 | if response['error'] is not None: 173 | raise ValueError(response['error']) 174 | elif 'result' not in response: 175 | raise ValueError({ 176 | 'code': -343, 'message': 'missing JSON-RPC result'}) 177 | else: 178 | return response['result'] 179 | 180 | def __getattr__(self, name): 181 | if name in self.__dict__: 182 | return self.__dict__[name] 183 | 184 | # Create a callable to do the actual call 185 | f = lambda *args: self._call(name, *args) 186 | 187 | # Make debuggers show rather than > 189 | f.__name__ = name 190 | return f 191 | 192 | 193 | class BitcoinD(TailableProc): 194 | 195 | CONF_NAME = 'bitcoin.conf' 196 | 197 | def __init__(self, bitcoin_dir="/tmp/bitcoind-test", rpcport=None): 198 | super().__init__(bitcoin_dir, 'bitcoind') 199 | 200 | if rpcport is None: 201 | rpcport = reserve() 202 | 203 | self.bitcoin_dir = bitcoin_dir 204 | 205 | self.prefix = 'bitcoind' 206 | BITCOIND_CONFIG['rpcport'] = rpcport 207 | self.rpcport = rpcport 208 | self.zmqpubrawblock_port = reserve() 209 | self.zmqpubrawtx_port = reserve() 210 | 211 | regtestdir = os.path.join(bitcoin_dir, 'regtest') 212 | if not os.path.exists(regtestdir): 213 | os.makedirs(regtestdir) 214 | 215 | conf_file = os.path.join(bitcoin_dir, self.CONF_NAME) 216 | 217 | self.cmd_line = [ 218 | 'bitcoind', 219 | '-datadir={}'.format(bitcoin_dir), 220 | '-conf={}'.format(conf_file), 221 | '-regtest', 222 | '-logtimestamps', 223 | '-rpcport={}'.format(rpcport), 224 | '-printtoconsole=1' 225 | '-debug', 226 | '-rpcuser=rpcuser', 227 | '-rpcpassword=rpcpass', 228 | '-zmqpubrawblock=tcp://127.0.0.1:{}'.format(self.zmqpubrawblock_port), 229 | '-zmqpubrawtx=tcp://127.0.0.1:{}'.format(self.zmqpubrawtx_port), 230 | ] 231 | BITCOIND_CONFIG['rpcport'] = rpcport 232 | write_config( 233 | os.path.join(bitcoin_dir, self.CONF_NAME), BITCOIND_CONFIG) 234 | write_config( 235 | os.path.join(regtestdir, self.CONF_NAME), BITCOIND_CONFIG) 236 | self.rpc = BitcoinRpc(rpcport=rpcport, rpcuser='rpcuser', rpcpassword='rpcpass') 237 | 238 | def start(self): 239 | super().start() 240 | self.wait_for_log("Done loading", timeout=10) 241 | 242 | logging.info("BitcoinD started") 243 | 244 | 245 | class BtcD(TailableProc): 246 | 247 | def __init__(self, btcdir="/tmp/btcd-test"): 248 | TailableProc.__init__(self, btcdir) 249 | 250 | self.cmd_line = [ 251 | 'btcd', 252 | '--regtest', 253 | '--rpcuser=rpcuser', 254 | '--rpcpass=rpcpass', 255 | '--connect=127.0.0.1', 256 | '--rpclisten=:18334', 257 | ] 258 | self.prefix = 'btcd' 259 | 260 | def start(self): 261 | TailableProc.start(self) 262 | self.wait_for_log("New valid peer 127.0.0.1:18444", timeout=10) 263 | 264 | logging.info("BtcD started") 265 | --------------------------------------------------------------------------------