From a3408e687cbdc5d226a5a8a4eb3fe136c322d5d9 Mon Sep 17 00:00:00 2001 From: yzernik Date: Sat, 7 May 2022 15:37:20 -0700 Subject: [PATCH] Add c-lightning support --- clightning-config.ini | 22 +++ docker/config-clightning.ini | 25 ++++ docker/docker-compose.mainnet.yml | 10 ++ docker/docker-compose.override.yml | 26 ++++ docker/docker-compose.yml | 65 +++++++++ requirements.txt | 2 + squeaknode/config/config.py | 11 ++ .../lightning/clightning_lightning_client.py | 133 ++++++++++++++++++ squeaknode/lightning/lightning_client.py | 7 + squeaknode/node/squeak_node.py | 19 ++- tests/core/test_squeak_core.py | 3 + 11 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 clightning-config.ini create mode 100644 docker/config-clightning.ini create mode 100644 squeaknode/lightning/clightning_lightning_client.py diff --git a/clightning-config.ini b/clightning-config.ini new file mode 100644 index 00000000..a9ae117b --- /dev/null +++ b/clightning-config.ini @@ -0,0 +1,22 @@ +[node] +network=testnet + +[clightning] +enabled=true + + +[bitcoin] +rpc_host=localhost +rpc_port=18332 +rpc_user=devuser +rpc_pass=devpass +zeromq_hashblock_port=28334 + +[tor] +proxy_ip=localhost +proxy_port=9150 + +[webadmin] +enabled=true +username=devuser +password=devpass diff --git a/docker/config-clightning.ini b/docker/config-clightning.ini new file mode 100644 index 00000000..3bed9167 --- /dev/null +++ b/docker/config-clightning.ini @@ -0,0 +1,25 @@ +[node] +network=testnet + +[lnd] +external_host=clightning_server +port=19735 + +[clightning] +enabled=true + +[bitcoin] +rpc_host=bitcoin-core +rpc_port=18332 +rpc_user=devuser +rpc_pass=devpass +zeromq_hashblock_port=28334 + +[tor] +proxy_ip=tor-node +proxy_port=9050 + +[webadmin] +enabled=true +username=devuser +password=devpass diff --git a/docker/docker-compose.mainnet.yml b/docker/docker-compose.mainnet.yml index 72c6c8dc..dcf9dbd1 100644 --- a/docker/docker-compose.mainnet.yml +++ b/docker/docker-compose.mainnet.yml @@ -19,6 +19,16 @@ services: environment: - NETWORK=mainnet + clightning_client: + command: + - --bitcoin-rpcconnect=bitcoin-core + - --bitcoin-rpcuser=devuser + - --bitcoin-rpcpassword=devpass + - --network=mainnet + - --plugin-dir=/usr/libexec/c-lightning/plugins + - --alias=myitestnode + - --log-level=debug + # squeaknode: # environment: # - NETWORK=mainnet diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml index 45560acb..30a92dfe 100644 --- a/docker/docker-compose.override.yml +++ b/docker/docker-compose.override.yml @@ -15,6 +15,32 @@ services: -zmqpubhashblock=tcp://0.0.0.0:28334 -zmqpubhashtx=tcp://0.0.0.0:28334 + clightning_client: + command: + - --bitcoin-rpcconnect=bitcoin-core + - --bitcoin-rpcuser=devuser + - --bitcoin-rpcpassword=devpass + - --network=testnet + - --plugin-dir=/usr/libexec/c-lightning/plugins + - --alias=myclientningclient + - --log-level=debug + - --rpc-file=/root/.lightning/lightning-rpc + - --rpc-file-mode=0777 + - --proxy=tor-node:9050 + + clightning_server: + command: + - --bitcoin-rpcconnect=bitcoin-core + - --bitcoin-rpcuser=devuser + - --bitcoin-rpcpassword=devpass + - --network=testnet + - --plugin-dir=/usr/libexec/c-lightning/plugins + - --alias=myclientningserver + - --log-level=debug + - --rpc-file=/root/.lightning/lightning-rpc + - --rpc-file-mode=0777 + - --proxy=tor-node:9050 + # squeaknode: # environment: # - NETWORK=testnet diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1ec03b84..9e777408 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -57,6 +57,45 @@ services: - net.ipv6.conf.all.disable_ipv6=0 entrypoint: ["./start-lnd.sh"] + clightning_client: + image: elementsproject/lightningd:v0.11.0.1 + container_name: clightning_client + expose: + - "9735" + - "19735" + - "9835" + - "10009" + ports: + - 10010:10009 + - 9835:9835 + volumes: + - ~/.lightning:/root/.lightning + - tor-dir:/var/lib/tor + links: + - "bitcoin-core:bitcoin-core" + depends_on: + - "tor-node" + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + + clightning_server: + image: elementsproject/lightningd:v0.11.0.1 + container_name: clightning_server + expose: + - "9735" + - "19735" + - "9835" + - "10009" + volumes: + - clightning_server_dir:/root/.lightning + - tor-dir:/var/lib/tor + links: + - "bitcoin-core:bitcoin-core" + depends_on: + - "tor-node" + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + tor-node: image: osminogin/tor-simple container_name: tor-node @@ -89,10 +128,36 @@ services: - net.ipv6.conf.all.disable_ipv6=0 entrypoint: ["./entrypoint.sh"] + squeaknode_clightning: + image: squeaknode + container_name: squeaknode_clightning + build: + context: ../ + dockerfile: Dockerfile + depends_on: + - "lnd_server" + volumes: + - clightning_server_dir:/root/.lightning + - squeaknode_clightning_dir:/root/.sqk + - ./config-clightning.ini:/config.ini + ports: + - 18558:18555 + - 12996:12994 + links: + - "bitcoin-core:bitcoin-core" + - "lnd_server:lnd" + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + entrypoint: ["./entrypoint.sh"] + volumes: lnd_server_dir: driver: local + clightning_server_dir: + driver: local squeaknode_dir: driver: local + squeaknode_clightning_dir: + driver: local tor-dir: driver: local diff --git a/requirements.txt b/requirements.txt index dc5b373f..f14dffce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ alembic==1.7.1 +docutils==0.17.1 Flask==2.0.1 flask-cors==3.0.10 flask-login==0.5.0 @@ -7,6 +8,7 @@ googleapis-common-protos==1.53.0 importlib_resources==1.4.0 mypy-protobuf==2.9 protobuf==3.17.3 +pyln-client==0.10.1 PySocks==1.7.1 python-bitcoinlib==0.11.0 pyzmq==22.3.0 diff --git a/squeaknode/config/config.py b/squeaknode/config/config.py index 82810f6b..7bb57efb 100644 --- a/squeaknode/config/config.py +++ b/squeaknode/config/config.py @@ -56,6 +56,9 @@ BITCOIN_RPC_PORT = { } DEFAULT_LND_PORT = 9735 DEFAULT_LND_RPC_PORT = 10009 +DEFAULT_CLIGHTNING_DIR = ".lightning" +DEFAULT_CLIGHTNING_RPC_FILE = str( + Path.home() / DEFAULT_CLIGHTNING_DIR / 'lightning-rpc') DEFAULT_SQK_DIR = ".sqk" DEFAULT_SQK_DIR_PATH = str(Path.home() / DEFAULT_SQK_DIR) DEFAULT_LND_HOST = "localhost" @@ -92,6 +95,13 @@ class LndConfig(Config): macaroon_path = key(cast=str, required=False, default="") +@section('clightning') +class CLightningConfig(Config): + enabled = key(cast=bool, required=False, default=False) + rpc_file = key(cast=str, required=False, + default=DEFAULT_CLIGHTNING_RPC_FILE) + + @section('tor') class TorConfig(Config): proxy_ip = key(cast=str, required=False, default="") @@ -173,6 +183,7 @@ class TwitterConfig(Config): class SqueaknodeConfig(Config): bitcoin = group_key(BitcoinConfig) lnd = group_key(LndConfig) + clightning = group_key(CLightningConfig) tor = group_key(TorConfig) server = group_key(ServerConfig) rpc = group_key(RpcConfig) diff --git a/squeaknode/lightning/clightning_lightning_client.py b/squeaknode/lightning/clightning_lightning_client.py new file mode 100644 index 00000000..d17636ad --- /dev/null +++ b/squeaknode/lightning/clightning_lightning_client.py @@ -0,0 +1,133 @@ +# MIT License +# +# Copyright (c) 2020 Jonathan Zernik +# +# 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 logging +import time +import uuid + +from pyln.client import LightningRpc + +from squeaknode.core.exception import InvoiceSubscriptionError +from squeaknode.lightning.info import Info +from squeaknode.lightning.invoice import Invoice +from squeaknode.lightning.invoice_stream import InvoiceStream +from squeaknode.lightning.lightning_client import LightningClient +from squeaknode.lightning.pay_req import PayReq +from squeaknode.lightning.payment import Payment + + +logger = logging.getLogger(__name__) + + +class CLightningClient(LightningClient): + """Access a c-lightning instance using the Python API.""" + + def __init__( + self, + rpc_path: str, + ) -> None: + self.rpc_path = rpc_path + + def init(self): + # Create an instance of the LightningRpc object using the Core Lightning daemon on your computer. + logger.info('initializing clightning client: {}'.format(self.rpc_path)) + self.lrpc = LightningRpc(self.rpc_path) + + def pay_invoice(self, payment_request: str) -> Payment: + payment = self.lrpc.pay(payment_request) + if payment['status'] == 'complete': + return Payment( + payment_preimage=bytes.fromhex(payment['payment_preimage']), + payment_error='', + ) + else: + return Payment( + payment_preimage=b'', + payment_error='Payment failed.', + ) + + def get_info(self) -> Info: + info = self.lrpc.getinfo() + pubkey = info['id'] + binding = info.get('binding') + uris = [] + if binding: + for b in binding: + address = b['address'] + if ':' not in address: # TODO: Change type of uri to LightningAddress. + port = b['port'] + uri = f"{pubkey}@{address}:{port}" + uris.append(uri) + return Info( + uris=uris, + ) + + def decode_pay_req(self, payment_request: str) -> PayReq: + pay_req = self.lrpc.decodepay(payment_request) + return PayReq( + payment_hash=bytes.fromhex(pay_req['payment_hash']), + payment_point=b'', # TODO: Use real payment point. + num_msat=pay_req['amount_msat'].millisatoshis, + destination=pay_req['payee'], + timestamp=int(pay_req['created_at']), + expiry=int(pay_req['expiry']), + ) + + def create_invoice(self, preimage: bytes, amount_msat: int) -> Invoice: + created_invoice = self.lrpc.invoice( + amount_msat, + label=str(uuid.uuid4()), + description="Squeaknode invoice", + preimage=preimage.hex(), + ) + creation_time = int(time.time()) + expiry = int(created_invoice['expires_at']) - creation_time + return Invoice( + r_hash=bytes.fromhex(created_invoice['payment_hash']), + payment_request=created_invoice['bolt11'], + value_msat=amount_msat, + settled=False, + settle_index=0, + creation_date=creation_time, + expiry=expiry, + ) + + def subscribe_invoices(self, settle_index: int) -> InvoiceStream: + # subscribe_invoices_request = lnd_pb2.InvoiceSubscription( + # settle_index=settle_index, + # ) + # subscribe_result = self.stub.SubscribeInvoices( + # subscribe_invoices_request, + # ) + # return InvoiceStream( + # cancel=subscribe_result.cancel, + # result_stream=iter(subscribe_result), + # ) + def cancel_fn(): + return None + + def get_invoice_stream(): + raise InvoiceSubscriptionError() + + return InvoiceStream( + cancel=cancel_fn, + result_stream=get_invoice_stream(), + ) diff --git a/squeaknode/lightning/lightning_client.py b/squeaknode/lightning/lightning_client.py index b578da08..eded4497 100644 --- a/squeaknode/lightning/lightning_client.py +++ b/squeaknode/lightning/lightning_client.py @@ -34,6 +34,13 @@ logger = logging.getLogger(__name__) class LightningClient(ABC): + @abstractmethod + def init(self): + """Initialize the lightning client. + Returns: + None + """ + @abstractmethod def get_info(self) -> Info: """Get info about the lightning node. diff --git a/squeaknode/node/squeak_node.py b/squeaknode/node/squeak_node.py index a7857539..784b01c0 100644 --- a/squeaknode/node/squeak_node.py +++ b/squeaknode/node/squeak_node.py @@ -34,6 +34,7 @@ from squeaknode.core.squeak_core import SqueakCore from squeaknode.db.db_engine import get_connection_string from squeaknode.db.db_engine import get_engine from squeaknode.db.squeak_db import SqueakDb +from squeaknode.lightning.clightning_lightning_client import CLightningClient from squeaknode.lightning.lnd_lightning_client import LNDLightningClient from squeaknode.node.node_settings import NodeSettings from squeaknode.node.payment_processor import PaymentProcessor @@ -48,6 +49,7 @@ from squeaknode.server.app import SqueakPeerWebServer from squeaknode.server.squeak_peer_server_handler import SqueakPeerServerHandler from squeaknode.twitter.twitter_forwarder import TwitterForwarder + logger = logging.getLogger(__name__) @@ -118,12 +120,17 @@ class SqueakNode: self.node_settings = NodeSettings(self.squeak_db) def create_lightning_client(self): - self.lightning_client = LNDLightningClient( - self.config.lnd.host, - self.config.lnd.rpc_port, - self.config.lnd.tls_cert_path, - self.config.lnd.macaroon_path, - ) + if self.config.clightning.enabled: + self.lightning_client = CLightningClient( + self.config.clightning.rpc_file, + ) + else: + self.lightning_client = LNDLightningClient( + self.config.lnd.host, + self.config.lnd.rpc_port, + self.config.lnd.tls_cert_path, + self.config.lnd.macaroon_path, + ) def create_bitcoin_client(self): self.bitcoin_client = BitcoinCoreClient( diff --git a/tests/core/test_squeak_core.py b/tests/core/test_squeak_core.py index 03b63cfd..a42c0b8b 100644 --- a/tests/core/test_squeak_core.py +++ b/tests/core/test_squeak_core.py @@ -189,6 +189,9 @@ class MockLightningClient(LightningClient): self.payment = payment self.invoice_stream = invoice_stream + def init(self): + pass + def get_info(self): return self.info