From 22fe8c4f7986d0dff320bb37d7e1c50a6d355592 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 19 Jun 2023 16:25:08 +0200 Subject: [PATCH 1/3] Add python packaging --- bindings/python/pyproject.toml | 21 +++++++++++++++++++++ bindings/python/setup.cfg | 13 +++++++++++++ bindings/python/src/ldk_node/__init__.py | 1 + scripts/python_create_package.sh | 3 +++ scripts/uniffi_bindgen_generate_python.sh | 6 +++--- 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 bindings/python/pyproject.toml create mode 100644 bindings/python/setup.cfg create mode 100644 bindings/python/src/ldk_node/__init__.py create mode 100755 scripts/python_create_package.sh diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 000000000..c315f991b --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "ldk_node" +version = "0.1-alpha.1" +authors = [ + { name="Elias Rohrer", email="dev@tnull.de" }, +] +description = "A ready-to-go Lightning node library built using LDK and BDK." +readme = "README.md" +requires-python = ">=3.6" +classifiers = [ + "Topic :: Software Development :: Libraries", + "Topic :: Security :: Cryptography", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", +] + +[project.urls] +"Homepage" = "https://lightningdevkit.org/" +"Github" = "https://github.com/lightningdevkit/ldk-node" +"Bug Tracker" = "https://github.com/lightningdevkit/ldk-node/issues" diff --git a/bindings/python/setup.cfg b/bindings/python/setup.cfg new file mode 100644 index 000000000..bd4e64216 --- /dev/null +++ b/bindings/python/setup.cfg @@ -0,0 +1,13 @@ +[options] +packages = find: +package_dir = + = src +include_package_data = True + +[options.packages.find] +where = src + +[options.package_data] +ldk_node = + *.so + *.dylib diff --git a/bindings/python/src/ldk_node/__init__.py b/bindings/python/src/ldk_node/__init__.py new file mode 100644 index 000000000..cd03b9139 --- /dev/null +++ b/bindings/python/src/ldk_node/__init__.py @@ -0,0 +1 @@ +from ldk_node.ldk_node import * diff --git a/scripts/python_create_package.sh b/scripts/python_create_package.sh new file mode 100755 index 000000000..0a993c9cb --- /dev/null +++ b/scripts/python_create_package.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd bindings/python || exit 1 +python3 -m build diff --git a/scripts/uniffi_bindgen_generate_python.sh b/scripts/uniffi_bindgen_generate_python.sh index 75a91482d..6604cec49 100755 --- a/scripts/uniffi_bindgen_generate_python.sh +++ b/scripts/uniffi_bindgen_generate_python.sh @@ -1,7 +1,7 @@ #!/bin/bash -BINDINGS_DIR="./bindings/python" +BINDINGS_DIR="./bindings/python/src/ldk_node" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" -cargo build --release --features uniffi || exit 1 +cargo build --profile release-smaller --features uniffi || exit 1 $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language python -o "$BINDINGS_DIR" || exit 1 -cp ./target/release/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1 +cp ./target/release-smaller/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1 From 8d91c24a2d1762a59388c980c1a8762700656d05 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 10 Nov 2023 15:49:30 +0100 Subject: [PATCH 2/3] Add `full_cycle` unit test in Python --- bindings/python/src/ldk_node/test_ldk_node.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 bindings/python/src/ldk_node/test_ldk_node.py diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py new file mode 100644 index 000000000..7c22d6cf8 --- /dev/null +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -0,0 +1,235 @@ +import unittest +import tempfile +import time +import subprocess +import os +import re +import requests + +from ldk_node import * + +DEFAULT_ESPLORA_SERVER_URL = "http://127.0.0.1:3002" +DEFAULT_TEST_NETWORK = Network.REGTEST +DEFAULT_BITCOIN_CLI_BIN = "bitcoin-cli" + +def bitcoin_cli(cmd): + args = [] + + bitcoin_cli_bin = [DEFAULT_BITCOIN_CLI_BIN] + if os.environ.get('BITCOIN_CLI_BIN'): + bitcoin_cli_bin = os.environ['BITCOIN_CLI_BIN'].split() + + args += bitcoin_cli_bin + args += ["-regtest"] + + if os.environ.get('BITCOIND_RPC_USER'): + args += ["-rpcuser=" + os.environ['BITCOIND_RPC_USER']] + + if os.environ.get('BITCOIND_RPC_PASSWORD'): + args += ["-rpcpassword=" + os.environ['BITCOIND_RPC_PASSWORD']] + + for c in cmd.split(): + args += [c] + + print("RUNNING:", args) + res = subprocess.run(args, capture_output=True) + return str(res.stdout.decode("utf-8")) + +def mine(blocks): + address = bitcoin_cli("getnewaddress").strip() + mining_res = bitcoin_cli("generatetoaddress " + str(blocks) + " " + str(address)) + print("MINING_RES:", mining_res) + + m = re.search("\\n.+\n\\]$", mining_res) + last_block = str(m.group(0)) + return str(last_block.strip().replace('"','').replace('\n]','')) + +def mine_and_wait(esplora_endpoint, blocks): + last_block = mine(blocks) + wait_for_block(esplora_endpoint, last_block) + +def wait_for_block(esplora_endpoint, block_hash): + url = esplora_endpoint + "/block/" + block_hash + "/status" + esplora_picked_up_block = False + while not esplora_picked_up_block: + res = requests.get(url) + try: + json = res.json() + esplora_picked_up_block = json['in_best_chain'] + except: + pass + time.sleep(1) + +def wait_for_tx(esplora_endpoint, txid): + url = esplora_endpoint + "/tx/" + txid + esplora_picked_up_tx = False + while not esplora_picked_up_tx: + res = requests.get(url) + try: + json = res.json() + esplora_picked_up_tx = json['txid'] == txid + except: + pass + time.sleep(1) + +def send_to_address(address, amount_sats): + amount_btc = amount_sats/100000000.0 + cmd = "sendtoaddress " + str(address) + " " + str(amount_btc) + res = str(bitcoin_cli(cmd)).strip() + print("SEND TX:", res) + return res + + +def setup_node(tmp_dir, esplora_endpoint, listening_address): + config = Config() + builder = Builder.from_config(config) + builder.set_storage_dir_path(tmp_dir) + builder.set_esplora_server(esplora_endpoint) + builder.set_network(DEFAULT_TEST_NETWORK) + builder.set_listening_address(listening_address) + return builder.build() + +def get_esplora_endpoint(): + if os.environ.get('ESPLORA_ENDPOINT'): + return str(os.environ['ESPLORA_ENDPOINT']) + return DEFAULT_ESPLORA_SERVER_URL + +class TestLdkNode(unittest.TestCase): + def setUp(self): + bitcoin_cli("createwallet ldk_node_test") + mine(101) + time.sleep(3) + esplora_endpoint = get_esplora_endpoint() + mine_and_wait(esplora_endpoint, 1) + + def test_channel_full_cycle(self): + esplora_endpoint = get_esplora_endpoint() + + ## Setup Node 1 + tmp_dir_1 = tempfile.TemporaryDirectory("_ldk_node_1") + print("TMP DIR 1:", tmp_dir_1.name) + + listening_address_1 = "127.0.0.1:2323" + node_1 = setup_node(tmp_dir_1.name, esplora_endpoint, listening_address_1) + node_1.start() + node_id_1 = node_1.node_id() + print("Node ID 1:", node_id_1) + + # Setup Node 2 + tmp_dir_2 = tempfile.TemporaryDirectory("_ldk_node_2") + print("TMP DIR 2:", tmp_dir_2.name) + + listening_address_2 = "127.0.0.1:2324" + node_2 = setup_node(tmp_dir_2.name, esplora_endpoint, listening_address_2) + node_2.start() + node_id_2 = node_2.node_id() + print("Node ID 2:", node_id_2) + + address_1 = node_1.new_onchain_address() + txid_1 = send_to_address(address_1, 100000) + address_2 = node_2.new_onchain_address() + txid_2 = send_to_address(address_2, 100000) + + wait_for_tx(esplora_endpoint, txid_1) + wait_for_tx(esplora_endpoint, txid_2) + + mine_and_wait(esplora_endpoint, 6) + + node_1.sync_wallets() + node_2.sync_wallets() + + spendable_balance_1 = node_1.spendable_onchain_balance_sats() + spendable_balance_2 = node_2.spendable_onchain_balance_sats() + total_balance_1 = node_1.total_onchain_balance_sats() + total_balance_2 = node_2.total_onchain_balance_sats() + + print("SPENDABLE 1:", spendable_balance_1) + self.assertEqual(spendable_balance_1, 100000) + + print("SPENDABLE 2:", spendable_balance_2) + self.assertEqual(spendable_balance_2, 100000) + + print("TOTAL 1:", total_balance_1) + self.assertEqual(total_balance_1, 100000) + + print("TOTAL 2:", total_balance_2) + self.assertEqual(total_balance_2, 100000) + + node_1.connect_open_channel(node_id_2, listening_address_2, 50000, None, None, True) + + channel_pending_event_1 = node_1.wait_next_event() + assert isinstance(channel_pending_event_1, Event.CHANNEL_PENDING) + print("EVENT:", channel_pending_event_1) + node_1.event_handled() + + channel_pending_event_2 = node_2.wait_next_event() + assert isinstance(channel_pending_event_2, Event.CHANNEL_PENDING) + print("EVENT:", channel_pending_event_2) + node_2.event_handled() + + funding_txid = channel_pending_event_1.funding_txo.txid + wait_for_tx(esplora_endpoint, funding_txid) + mine_and_wait(esplora_endpoint, 6) + + node_1.sync_wallets() + node_2.sync_wallets() + + channel_ready_event_1 = node_1.wait_next_event() + assert isinstance(channel_ready_event_1, Event.CHANNEL_READY) + print("EVENT:", channel_ready_event_1) + print("funding_txo:", funding_txid) + node_1.event_handled() + + channel_ready_event_2 = node_2.wait_next_event() + assert isinstance(channel_ready_event_2, Event.CHANNEL_READY) + print("EVENT:", channel_ready_event_2) + node_2.event_handled() + + invoice = node_2.receive_payment(2500000, "asdf", 9217) + node_1.send_payment(invoice) + + payment_successful_event_1 = node_1.wait_next_event() + assert isinstance(payment_successful_event_1, Event.PAYMENT_SUCCESSFUL) + print("EVENT:", payment_successful_event_1) + node_1.event_handled() + + payment_received_event_2 = node_2.wait_next_event() + assert isinstance(payment_received_event_2, Event.PAYMENT_RECEIVED) + print("EVENT:", payment_received_event_2) + node_2.event_handled() + + node_2.close_channel(channel_ready_event_2.channel_id, node_id_1) + + channel_closed_event_1 = node_1.wait_next_event() + assert isinstance(channel_closed_event_1, Event.CHANNEL_CLOSED) + print("EVENT:", channel_closed_event_1) + node_1.event_handled() + + channel_closed_event_2 = node_2.wait_next_event() + assert isinstance(channel_closed_event_2, Event.CHANNEL_CLOSED) + print("EVENT:", channel_closed_event_2) + node_2.event_handled() + + mine_and_wait(esplora_endpoint, 1) + + node_1.sync_wallets() + node_2.sync_wallets() + + spendable_balance_after_close_1 = node_1.spendable_onchain_balance_sats() + assert spendable_balance_after_close_1 > 95000 + assert spendable_balance_after_close_1 < 100000 + spendable_balance_after_close_2 = node_2.spendable_onchain_balance_sats() + self.assertEqual(spendable_balance_after_close_2, 102500) + + # Stop nodes + node_1.stop() + node_2.stop() + + # Cleanup + time.sleep(1) # Wait a sec so our logs can finish writing + tmp_dir_1.cleanup() + tmp_dir_2.cleanup() + +if __name__ == '__main__': + unittest.main() + From 67dbc56bfb4df8d54a12a1a8287e532ab9330d8f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 10 Nov 2023 15:59:22 +0100 Subject: [PATCH 3/3] Add Python CI job --- .github/workflows/python.yml | 39 +++++++++++++++++++++++ scripts/uniffi_bindgen_generate_python.sh | 10 +++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 000000000..4269c69bf --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,39 @@ +name: Continuous Integration Checks - Python + +on: [push, pull_request] + +jobs: + check-python: + runs-on: ubuntu-latest + + env: + LDK_NODE_PYTHON_DIR: bindings/python + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Generate Python bindings + run: ./scripts/uniffi_bindgen_generate_python.sh + + - name: Start bitcoind and electrs + run: docker compose up -d + + - name: Install testing prerequisites + run: | + pip3 install requests + + - name: Run Python unit tests + env: + BITCOIN_CLI_BIN: "docker exec ldk-node-bitcoin-1 bitcoin-cli" + BITCOIND_RPC_USER: "user" + BITCOIND_RPC_PASSWORD: "pass" + ESPLORA_ENDPOINT: "http://127.0.0.1:3002" + run: | + cd $LDK_NODE_PYTHON_DIR + python3 -m unittest discover -s src/ldk_node diff --git a/scripts/uniffi_bindgen_generate_python.sh b/scripts/uniffi_bindgen_generate_python.sh index 6604cec49..14e7b4f86 100755 --- a/scripts/uniffi_bindgen_generate_python.sh +++ b/scripts/uniffi_bindgen_generate_python.sh @@ -2,6 +2,14 @@ BINDINGS_DIR="./bindings/python/src/ldk_node" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.so" +else + DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.dylib" +fi + cargo build --profile release-smaller --features uniffi || exit 1 $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language python -o "$BINDINGS_DIR" || exit 1 -cp ./target/release-smaller/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1 + +mkdir -p $BINDINGS_DIR +cp "$DYNAMIC_LIB_PATH" "$BINDINGS_DIR" || exit 1