├── .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 |
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 | Client
18 | Commit
19 |
20 |
21 |
22 | {% for k, v in versions|dictsort %}
23 |
24 | {{ k }}
25 | {{ v }}
26 | {% endfor %}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Test
35 | Configuration
36 | Result
37 |
38 |
39 |
40 | {% for name, test in tests|dictsort %}
41 |
42 | {{ name }}
43 |
44 | {{ test.success }}/{{ test.total }}
45 |
46 | {% for stest in test.subtests %}
47 |
48 |
49 | {{ stest.name.replace("_", " ") }}
50 | {{ stest.call.outcome }}
51 |
52 |
53 | {% endfor %}
54 | {% endfor %}
55 |
56 |
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 | ID
30 | Date/Time
31 | Result
32 |
33 |
34 |
35 | {% for report in reports %}
36 |
37 | {{ report.id[:7] }}
38 | {{ report.created_at[:19] }}
39 |
40 |
41 | {{ report.summary.passed }} / {{report.summary.num_tests }}
42 |
43 |
44 |
45 | {% endfor %}
46 |
47 |
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 |
--------------------------------------------------------------------------------