Browse Source

persist channel db on disk. verify channel gossip sigs.

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
SomberNight 7 years ago
committed by ThomasV
parent
commit
a5b44d25b0
  1. 5
      electrum/constants.py
  2. 2
      electrum/ecc.py
  3. 168
      electrum/lnchanannverifier.py
  4. 180
      electrum/lnrouter.py
  5. 13
      electrum/lnutil.py
  6. 3
      electrum/network.py
  7. 76
      electrum/tests/test_lnrouter.py
  8. 4
      electrum/util.py

5
electrum/constants.py

@ -27,6 +27,7 @@ import os
import json
from .util import inv_dict
from . import bitcoin
def read_json(filename, default):
@ -49,6 +50,10 @@ class AbstractNet:
def max_checkpoint(cls) -> int:
return max(0, len(cls.CHECKPOINTS) * 2016 - 1)
@classmethod
def rev_genesis_bytes(cls) -> bytes:
return bytes.fromhex(bitcoin.rev_hex(cls.GENESIS))
class BitcoinMainnet(AbstractNet):

2
electrum/ecc.py

@ -313,7 +313,7 @@ def msg_magic(message: bytes) -> bytes:
return b"\x18Bitcoin Signed Message:\n" + length + message
def verify_signature(pubkey, sig, h):
def verify_signature(pubkey: bytes, sig: bytes, h: bytes) -> bool:
try:
ECPubkey(pubkey).verify_message_hash(sig, h)
except:

168
electrum/lnchanannverifier.py

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import threading
from . import lnbase
from . import bitcoin
from . import ecc
from .util import ThreadJob, bh2u, bfh
from .lnutil import invert_short_channel_id, funding_output_script_from_keys
from .verifier import verify_tx_is_in_block, MerkleVerificationFailure
from .transaction import Transaction
class LNChanAnnVerifier(ThreadJob):
""" Verify channel announcements for the Channel DB """
def __init__(self, network, channel_db):
self.network = network
self.channel_db = channel_db
self.lock = threading.Lock()
# items only removed when whole verification succeeds for them.
# fixme: if it fails, it will never succeed
self.started_verifying_channel = set() # short_channel_id
self.unverified_channel_info = {} # short_channel_id -> channel_info
def add_new_channel_info(self, channel_info):
short_channel_id = channel_info.channel_id
if short_channel_id in self.unverified_channel_info:
return
if not verify_sigs_for_channel_announcement(channel_info.msg_payload):
return
with self.lock:
self.unverified_channel_info[short_channel_id] = channel_info
def get_pending_channel_info(self, short_channel_id):
return self.unverified_channel_info.get(short_channel_id, None)
def run(self):
interface = self.network.interface
if not interface:
return
blockchain = interface.blockchain
if not blockchain:
return
with self.lock:
unverified_channel_info = list(self.unverified_channel_info)
for short_channel_id in unverified_channel_info:
if short_channel_id in self.started_verifying_channel:
continue
block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id)
# only resolve short_channel_id if headers are available.
header = blockchain.read_header(block_height)
if header is None:
index = block_height // 2016
if index < len(blockchain.checkpoints):
self.network.request_chunk(interface, index)
continue
callback = lambda resp, short_channel_id=short_channel_id: self.on_txid_and_merkle(resp, short_channel_id)
self.network.get_txid_from_txpos(block_height, tx_pos, True,
callback=callback)
#self.print_error('requested short_channel_id', bh2u(short_channel_id))
with self.lock:
self.started_verifying_channel.add(short_channel_id)
def on_txid_and_merkle(self, response, short_channel_id):
if response.get('error'):
self.print_error('received an error:', response)
return
result = response['result']
tx_hash = result['tx_hash']
merkle_branch = result['merkle']
block_height, tx_pos, output_idx = invert_short_channel_id(short_channel_id)
header = self.network.blockchain().read_header(block_height)
try:
verify_tx_is_in_block(tx_hash, merkle_branch, tx_pos, header, block_height)
except MerkleVerificationFailure as e:
self.print_error(str(e))
return
callback = lambda resp, short_channel_id=short_channel_id: self.on_tx_response(resp, short_channel_id)
self.network.get_transaction(tx_hash, callback=callback)
def on_tx_response(self, response, short_channel_id):
if response.get('error'):
self.print_error('received an error:', response)
return
params = response['params']
result = response['result']
tx_hash = params[0]
tx = Transaction(result)
try:
tx.deserialize()
except Exception:
self.print_msg("cannot deserialize transaction, skipping", tx_hash)
return
if tx_hash != tx.txid():
self.print_error("received tx does not match expected txid ({} != {})"
.format(tx_hash, tx.txid()))
return
# check funding output
channel_info = self.unverified_channel_info[short_channel_id]
chan_ann = channel_info.msg_payload
redeem_script = funding_output_script_from_keys(chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2'])
expected_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
output_idx = invert_short_channel_id(short_channel_id)[2]
try:
actual_output = tx.outputs()[output_idx]
except IndexError:
return
if expected_address != actual_output[1]:
return
# put channel into channel DB
channel_info.set_capacity(actual_output[2])
self.channel_db.add_verified_channel_info(short_channel_id, channel_info)
# remove channel from unverified
with self.lock:
self.unverified_channel_info.pop(short_channel_id, None)
try: self.started_verifying_channel.remove(short_channel_id)
except KeyError: pass
def verify_sigs_for_channel_announcement(chan_ann: dict) -> bool:
msg_bytes = lnbase.gen_msg('channel_announcement', **chan_ann)
pre_hash = msg_bytes[2+256:]
h = bitcoin.Hash(pre_hash)
pubkeys = [chan_ann['node_id_1'], chan_ann['node_id_2'], chan_ann['bitcoin_key_1'], chan_ann['bitcoin_key_2']]
sigs = [chan_ann['node_signature_1'], chan_ann['node_signature_2'], chan_ann['bitcoin_signature_1'], chan_ann['bitcoin_signature_2']]
for pubkey, sig in zip(pubkeys, sigs):
if not ecc.verify_signature(pubkey, sig, h):
return False
return True
def verify_sig_for_channel_update(chan_upd: dict, node_id: bytes) -> bool:
msg_bytes = lnbase.gen_msg('channel_update', **chan_upd)
pre_hash = msg_bytes[2+64:]
h = bitcoin.Hash(pre_hash)
sig = chan_upd['signature']
if not ecc.verify_signature(node_id, sig, h):
return False
return True

180
electrum/lnrouter.py

@ -30,8 +30,11 @@ import sys
import binascii
import hashlib
import hmac
import os
import json
import threading
from collections import namedtuple, defaultdict
from typing import Sequence, Union, Tuple
from typing import Sequence, Union, Tuple, Optional
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
@ -39,9 +42,12 @@ from cryptography.hazmat.backends import default_backend
from . import bitcoin
from . import ecc
from . import crypto
from . import constants
from .crypto import sha256
from .util import PrintError, bh2u, profiler, xor_bytes
from .util import PrintError, bh2u, profiler, xor_bytes, get_headers_dir, bfh
from .lnutil import get_ecdh
from .storage import JsonDB
from .lnchanannverifier import LNChanAnnVerifier, verify_sig_for_channel_update
class ChannelInfo(PrintError):
@ -54,23 +60,71 @@ class ChannelInfo(PrintError):
assert type(self.node_id_2) is bytes
assert list(sorted([self.node_id_1, self.node_id_2])) == [self.node_id_1, self.node_id_2]
self.features_len = channel_announcement_payload['len']
self.features = channel_announcement_payload['features']
self.bitcoin_key_1 = channel_announcement_payload['bitcoin_key_1']
self.bitcoin_key_2 = channel_announcement_payload['bitcoin_key_2']
# this field does not get persisted
self.msg_payload = channel_announcement_payload
self.capacity_sat = None
self.policy_node1 = None
self.policy_node2 = None
def to_json(self) -> dict:
d = {}
d['short_channel_id'] = bh2u(self.channel_id)
d['node_id_1'] = bh2u(self.node_id_1)
d['node_id_2'] = bh2u(self.node_id_2)
d['len'] = bh2u(self.features_len)
d['features'] = bh2u(self.features)
d['bitcoin_key_1'] = bh2u(self.bitcoin_key_1)
d['bitcoin_key_2'] = bh2u(self.bitcoin_key_2)
d['policy_node1'] = self.policy_node1
d['policy_node2'] = self.policy_node2
d['capacity_sat'] = self.capacity_sat
return d
@classmethod
def from_json(cls, d: dict):
d2 = {}
d2['short_channel_id'] = bfh(d['short_channel_id'])
d2['node_id_1'] = bfh(d['node_id_1'])
d2['node_id_2'] = bfh(d['node_id_2'])
d2['len'] = bfh(d['len'])
d2['features'] = bfh(d['features'])
d2['bitcoin_key_1'] = bfh(d['bitcoin_key_1'])
d2['bitcoin_key_2'] = bfh(d['bitcoin_key_2'])
ci = ChannelInfo(d2)
ci.capacity_sat = d['capacity_sat']
ci.policy_node1 = ChannelInfoDirectedPolicy.from_json(d['policy_node1'])
ci.policy_node2 = ChannelInfoDirectedPolicy.from_json(d['policy_node2'])
return ci
def set_capacity(self, capacity):
# TODO call this after looking up UTXO for funding txn on chain
self.capacity_sat = capacity
def on_channel_update(self, msg_payload):
assert self.channel_id == msg_payload['short_channel_id']
flags = int.from_bytes(msg_payload['flags'], 'big')
direction = flags & 1
new_policy = ChannelInfoDirectedPolicy(msg_payload)
if direction == 0:
old_policy = self.policy_node1
node_id = self.node_id_1
else:
old_policy = self.policy_node2
node_id = self.node_id_2
if old_policy and old_policy.timestamp >= new_policy.timestamp:
return # ignore
if not verify_sig_for_channel_update(msg_payload, node_id):
return # ignore
# save new policy
if direction == 0:
self.policy_node1 = ChannelInfoDirectedPolicy(msg_payload)
self.policy_node1 = new_policy
else:
self.policy_node2 = ChannelInfoDirectedPolicy(msg_payload)
#self.print_error('channel update', binascii.hexlify(self.channel_id).decode("ascii"), flags)
self.policy_node2 = new_policy
def get_policy_for_node(self, node_id):
if node_id == self.node_id_1:
@ -84,51 +138,121 @@ class ChannelInfo(PrintError):
class ChannelInfoDirectedPolicy:
def __init__(self, channel_update_payload):
self.cltv_expiry_delta = channel_update_payload['cltv_expiry_delta']
self.htlc_minimum_msat = channel_update_payload['htlc_minimum_msat']
self.fee_base_msat = channel_update_payload['fee_base_msat']
self.fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
self.cltv_expiry_delta = int.from_bytes(self.cltv_expiry_delta, "big")
self.htlc_minimum_msat = int.from_bytes(self.htlc_minimum_msat, "big")
self.fee_base_msat = int.from_bytes(self.fee_base_msat, "big")
self.fee_proportional_millionths = int.from_bytes(self.fee_proportional_millionths, "big")
cltv_expiry_delta = channel_update_payload['cltv_expiry_delta']
htlc_minimum_msat = channel_update_payload['htlc_minimum_msat']
fee_base_msat = channel_update_payload['fee_base_msat']
fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
flags = channel_update_payload['flags']
timestamp = channel_update_payload['timestamp']
self.cltv_expiry_delta = int.from_bytes(cltv_expiry_delta, "big")
self.htlc_minimum_msat = int.from_bytes(htlc_minimum_msat, "big")
self.fee_base_msat = int.from_bytes(fee_base_msat, "big")
self.fee_proportional_millionths = int.from_bytes(fee_proportional_millionths, "big")
self.flags = int.from_bytes(flags, "big")
self.timestamp = int.from_bytes(timestamp, "big")
def to_json(self) -> dict:
d = {}
d['cltv_expiry_delta'] = self.cltv_expiry_delta
d['htlc_minimum_msat'] = self.htlc_minimum_msat
d['fee_base_msat'] = self.fee_base_msat
d['fee_proportional_millionths'] = self.fee_proportional_millionths
d['flags'] = self.flags
d['timestamp'] = self.timestamp
return d
@classmethod
def from_json(cls, d: dict):
if d is None: return None
d2 = {}
d2['cltv_expiry_delta'] = d['cltv_expiry_delta'].to_bytes(2, "big")
d2['htlc_minimum_msat'] = d['htlc_minimum_msat'].to_bytes(8, "big")
d2['fee_base_msat'] = d['fee_base_msat'].to_bytes(4, "big")
d2['fee_proportional_millionths'] = d['fee_proportional_millionths'].to_bytes(4, "big")
d2['flags'] = d['flags'].to_bytes(2, "big")
d2['timestamp'] = d['timestamp'].to_bytes(4, "big")
return ChannelInfoDirectedPolicy(d2)
class ChannelDB(JsonDB):
class ChannelDB(PrintError):
def __init__(self, network):
self.network = network
def __init__(self):
path = os.path.join(get_headers_dir(network.config), 'channel_db')
JsonDB.__init__(self, path)
self.lock = threading.Lock()
self._id_to_channel_info = {}
self._channels_for_node = defaultdict(set) # node -> set(short_channel_id)
self.ca_verifier = LNChanAnnVerifier(network, self)
self.network.add_jobs([self.ca_verifier])
self.load_data()
def load_data(self):
if os.path.exists(self.path):
with open(self.path, "r", encoding='utf-8') as f:
raw = f.read()
self.data = json.loads(raw)
channel_infos = self.get('channel_infos', {})
for short_channel_id, channel_info_d in channel_infos.items():
channel_info = ChannelInfo.from_json(channel_info_d)
short_channel_id = bfh(short_channel_id)
self.add_verified_channel_info(short_channel_id, channel_info)
def save_data(self):
with self.lock:
channel_infos = {}
for short_channel_id, channel_info in self._id_to_channel_info.items():
channel_infos[bh2u(short_channel_id)] = channel_info
self.put('channel_infos', channel_infos)
self.write()
def __len__(self):
return len(self._id_to_channel_info)
def get_channel_info(self, channel_id):
def get_channel_info(self, channel_id) -> Optional[ChannelInfo]:
return self._id_to_channel_info.get(channel_id, None)
def get_channels_for_node(self, node_id):
"""Returns the set of channels that have node_id as one of the endpoints."""
return self._channels_for_node[node_id]
def add_verified_channel_info(self, short_channel_id: bytes, channel_info: ChannelInfo):
with self.lock:
self._id_to_channel_info[short_channel_id] = channel_info
self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
def on_channel_announcement(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
#self.print_error('channel announcement', binascii.hexlify(short_channel_id).decode("ascii"))
channel_info = ChannelInfo(msg_payload)
if short_channel_id in self._id_to_channel_info:
self.print_error("IGNORING CHANNEL ANNOUNCEMENT, WE ALREADY KNOW THIS CHANNEL")
return
self._id_to_channel_info[short_channel_id] = channel_info
self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
if constants.net.rev_genesis_bytes() != msg_payload['chain_hash']:
return
channel_info = ChannelInfo(msg_payload)
self.ca_verifier.add_new_channel_info(channel_info)
def on_channel_update(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
try:
channel_info = self._id_to_channel_info[short_channel_id]
except KeyError:
if constants.net.rev_genesis_bytes() != msg_payload['chain_hash']:
return
# try finding channel in verified db
channel_info = self._id_to_channel_info.get(short_channel_id, None)
if channel_info is None:
# try finding channel in pending db
channel_info = self.ca_verifier.get_pending_channel_info(short_channel_id)
if channel_info is None:
# try finding channel in verified db, again
# (maybe this is redundant but this should prevent a race..)
channel_info = self._id_to_channel_info.get(short_channel_id, None)
if channel_info is None:
self.print_error("could not find", short_channel_id)
else:
channel_info.on_channel_update(msg_payload)
return
channel_info.on_channel_update(msg_payload)
def remove_channel(self, short_channel_id):
try:

13
electrum/lnutil.py

@ -344,8 +344,11 @@ def sign_and_get_sig_string(tx, local_config, remote_config):
sig_64 = sig_string_from_der_sig(sig[:-1])
return sig_64
def funding_output_script(local_config, remote_config):
pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
def funding_output_script(local_config, remote_config) -> str:
return funding_output_script_from_keys(local_config.multisig_key.pubkey, remote_config.multisig_key.pubkey)
def funding_output_script_from_keys(pubkey1: bytes, pubkey2: bytes) -> str:
pubkeys = sorted([bh2u(pubkey1), bh2u(pubkey2)])
return transaction.multisig_script(pubkeys, 2)
def calc_short_channel_id(block_height: int, tx_pos_in_block: int, output_index: int) -> bytes:
@ -354,6 +357,12 @@ def calc_short_channel_id(block_height: int, tx_pos_in_block: int, output_index:
oi = output_index.to_bytes(2, byteorder='big')
return bh + tpos + oi
def invert_short_channel_id(short_channel_id: bytes) -> (int, int, int):
bh = int.from_bytes(short_channel_id[:3], byteorder='big')
tpos = int.from_bytes(short_channel_id[3:6], byteorder='big')
oi = int.from_bytes(short_channel_id[6:8], byteorder='big')
return bh, tpos, oi
def get_obscured_ctn(ctn, local, remote):
mask = int.from_bytes(sha256(local + remote)[-6:], 'big')
return ctn ^ mask

3
electrum/network.py

@ -301,7 +301,7 @@ class Network(Logger):
# lightning network
self.lightning_nodes = {}
self.channel_db = lnrouter.ChannelDB()
self.channel_db = lnrouter.ChannelDB(self)
self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.lnwatcher = lnwatcher.LNWatcher(self)
@ -1183,6 +1183,7 @@ class Network(Logger):
def stop(self):
assert self._loop_thread != threading.current_thread(), 'must not be called from network thread'
fut = asyncio.run_coroutine_threadsafe(self._stop(full_shutdown=True), self.asyncio_loop)
self.channel_db.save_data()
try:
fut.result(timeout=2)
except (asyncio.TimeoutError, asyncio.CancelledError): pass

76
electrum/tests/test_lnrouter.py

@ -1,11 +1,17 @@
import unittest
import tempfile
import shutil
from electrum.util import bh2u, bfh
from electrum.lnbase import Peer
from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop
from electrum import bitcoin, lnrouter
from electrum.simple_config import SimpleConfig
from electrum import lnchanannverifier
class Test_LNRouter(unittest.TestCase):
from . import TestCaseForTestnet
class Test_LNRouter(TestCaseForTestnet):
#@staticmethod
#def parse_witness_list(witness_bytes):
@ -21,12 +27,26 @@ class Test_LNRouter(unittest.TestCase):
# assert witness_bytes == b"", witness_bytes
# return res
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.electrum_path = tempfile.mkdtemp()
cls.config = SimpleConfig({'electrum_path': cls.electrum_path})
@classmethod
def tearDownClass(cls):
super().tearDownClass()
shutil.rmtree(cls.electrum_path)
def test_find_path_for_payment(self):
class fake_network:
channel_db = lnrouter.ChannelDB()
config = self.config
trigger_callback = lambda x: None
add_jobs = lambda *args: None
fake_network.channel_db = lnrouter.ChannelDB(fake_network())
def no_verify_add_new_channel_info(channel_info):
fake_network.channel_db.add_verified_channel_info(channel_info.channel_id, channel_info)
fake_network.channel_db.ca_verifier.add_new_channel_info = no_verify_add_new_channel_info
class fake_ln_worker:
path_finder = lnrouter.LNPathFinder(fake_network.channel_db)
privkey = bitcoin.sha256('privkeyseed')
@ -34,27 +54,39 @@ class Test_LNRouter(unittest.TestCase):
channels = []
invoices = {}
channels_for_peer = lambda x: []
p = Peer(fake_ln_worker, '', 0, 'a')
p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'c', 'short_channel_id': bfh('0000000000000001')})
p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000002')})
p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'b', 'short_channel_id': bfh('0000000000000003')})
p.on_channel_announcement({'node_id_1': b'c', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000004')})
p.on_channel_announcement({'node_id_1': b'd', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000005')})
p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000006')})
p = Peer(fake_ln_worker, '', 0, b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
p.on_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'node_id_2': b'\x02cccccccccccccccccccccccccccccccc',
'short_channel_id': bfh('0000000000000001'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'short_channel_id': bfh('0000000000000002'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'short_channel_id': bfh('0000000000000003'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc',
'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
'short_channel_id': bfh('0000000000000004'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd',
'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'short_channel_id': bfh('0000000000000005'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
'short_channel_id': bfh('0000000000000006'), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
o = lambda i: i.to_bytes(8, "big")
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999)})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999)})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'a', b'e', 100000))
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': bfh('43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000')})
self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 100000))

4
electrum/util.py

@ -217,7 +217,9 @@ class MyEncoder(json.JSONEncoder):
return obj.isoformat(' ')[:-3]
if isinstance(obj, set):
return list(obj)
return super().default(obj)
if hasattr(obj, 'to_json') and callable(obj.to_json):
return obj.to_json()
return super(MyEncoder, self).default(obj)
class ThreadJob(Logger):

Loading…
Cancel
Save