From 4143b92394961b1ff49f15a0b86a0ac39c851e76 Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Tue, 16 Apr 2024 12:15:41 -0400 Subject: [PATCH] Added python getting-started example. --- docs/mkdocs.yml | 2 +- docs/src/getting-started/recover.md | 15 +-- docs/src/getting-started/register.md | 43 +------- docs/src/getting-started/schedule.md | 26 +---- examples/python/getting-started/ca.crt | 15 +++ examples/python/getting-started/main.py | 120 ++++++++++++++++++++++ examples/rust/getting-started/src/main.rs | 16 ++- 7 files changed, 157 insertions(+), 80 deletions(-) create mode 100644 examples/python/getting-started/ca.crt create mode 100644 examples/python/getting-started/main.py diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 904160274..8ec245611 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -29,7 +29,7 @@ markdown_extensions: strip_comments: true strip_js_on_attributes: false - pymdownx.snippets: - base_path: ["../examples/rust/getting-started/src"] + base_path: ["../examples/rust/getting-started/src", "../examples/python/getting-started/"] theme: name: material logo: assets/logo.png diff --git a/docs/src/getting-started/recover.md b/docs/src/getting-started/recover.md index 52abfca9e..03533e7d2 100644 --- a/docs/src/getting-started/recover.md +++ b/docs/src/getting-started/recover.md @@ -16,20 +16,7 @@ In order to recover access all you need to do is recover the `seed` from the BIP === "Python" ```python - from glclient import Scheduler, Signer, TlsConfig - - cert = ... // Your developer certificate - key = ... // Your developer key - seed = ... // Load seed from file - - tls = TlsConfig().identity(cert, key); - signer = Signer(seed, network="bitcoin", tls=tls) - scheduler = Scheduler( - node_id=signer.node_id(), - network="bitcoin", - tls=tls, - ) - res = scheduler.recover(signer) +--8<-- "main.py:recover_node" ``` Notice that we are using a `TlsConfig` that is not configured with a diff --git a/docs/src/getting-started/register.md b/docs/src/getting-started/register.md index 4af59eb80..2e98b957a 100644 --- a/docs/src/getting-started/register.md +++ b/docs/src/getting-started/register.md @@ -56,17 +56,7 @@ phrase and then convert it into a seed secret we can use: === "Python" ```python - import bip39 - import secrets # Make sure to use cryptographically sound randomness - - rand = secrets.randbits(256).to_bytes(32, 'big') # 32 bytes of randomness - phrase = bip39.encode_bytes(rand) - - # Prompt user to safely store the phrase - - seed = bip39.phrase_to_seed(phrase)[:32] # Only need 32 bytes - - # Store the seed on the filesystem, or secure configuration system +--8<-- "main.py:create_seed" ``` !!! important @@ -89,12 +79,7 @@ nodes. === "Python" ```python - from glclient import TlsConfig - - # Creating a new `TlsConfig` object using your developer certificate - # - cert: contains the content of `client.crt` - # - key: contains the content of `client-key.pem` - tls = TlsConfig().identity(cert, key) +--8<-- "main.py:dev_creds" ``` @@ -115,9 +100,7 @@ We'll pick `bitcoin`, because ... reckless 😉 === "Python" ```python - from glclient import Signer - - signer = Signer(seed, network="bitcoin", tls=tls) +--8<-- "main.py:init_signer" ``` [bip39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki @@ -140,17 +123,7 @@ node's private key. Since the private key is managed exclusively by the === "Python" ```python - from glclient import Scheduler - - scheduler = Scheduler( - node_id=signer.node_id(), - network="bitcoin", - tls=tls, - ) - - # Passing in the signer is required because the client needs to prove - # ownership of the `node_id` - res = scheduler.register(signer) +--8<-- "main.py:register_node" ``` The result of `register` contains the credentials that can be used @@ -167,13 +140,7 @@ going forward to talk to the scheduler and the node itself. === "Python" ```python - tls = TlsConfig().identity(res.device_cert, res.device_key) - - # Use the configured `tls` instance when creating `Scheduler` and `Signer` - # instance going forward - signer = Signer(seed, network="bitcoin", tls=tls) - scheduler = Scheduler(node_id=signer.node_id(), network="bitcoin", tls=tls) - node = scheduler.node() +--8<-- "main.py:get_node" ``` If you get an error about a certificate verification failure when diff --git a/docs/src/getting-started/schedule.md b/docs/src/getting-started/schedule.md index 632b7f767..cb26aa9dc 100644 --- a/docs/src/getting-started/schedule.md +++ b/docs/src/getting-started/schedule.md @@ -31,14 +31,7 @@ Greenlight infrastructure: === "Python" ```python - from glclient import TlsConfig, Scheduler - cert, key = b'...', b'...' - node_id = bytes.fromhex("02058e8b6c2ad363ec59aa136429256d745164c2bdc87f98f0a68690ec2c5c9b0b") - network = "testnet" - tls = TlsConfig().identity(cert, key) - - scheduler = Scheduler(node_id, network, tls) - node = scheduler.node() +--8<-- "main.py:start_node" ``` Once we have an instance of the `Node` we can start interacting with it via the GRPC interface: @@ -49,8 +42,7 @@ Once we have an instance of the `Node` we can start interacting with it via the ``` === "Python" ```python - info = node.get_info() - peers = node.list_peers() +--8<-- "main.py:list_peers" ``` The above snippet will read the metadata and list the peers from the @@ -67,12 +59,7 @@ only component with access to your key. === "Python" ```python - from glclient import clnpb - node.invoice( - amount_msat=clnpb.AmountOrAny(any=True), - label="label", - description="description", - ) +--8<-- "main.py:create_invoice" ``` You'll notice that these calls hang indefinitely. This is because the @@ -96,12 +83,7 @@ in the last chapter, instantiate the signer with it and then start it. === "Python" ```python - seed = ... # Load from wherever you stored it - cert, key = ... // Load the cert and key you got from the `register` call - - tls = TlsConfig().identity(device_cert, device_key) - signer = Signer(seed, network="bitcoin", tls=tls) - signer.run_in_thread() +--8<-- "main.py:start_signer" ``` If you kept the stuck commands above running, you should notice that diff --git a/examples/python/getting-started/ca.crt b/examples/python/getting-started/ca.crt new file mode 100644 index 000000000..4fa003ea7 --- /dev/null +++ b/examples/python/getting-started/ca.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICYjCCAgigAwIBAgIUDEw2osNBr+H1o4WCvPSRIjNzUzQwCgYIKoZIzj0EAwIw +fjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xFDASBgNVBAoTC0Jsb2Nrc3RyZWFtMR0wGwYDVQQLExRDZXJ0 +aWZpY2F0ZUF1dGhvcml0eTENMAsGA1UEAxMER0wgLzAeFw0yMTA0MjYxNzE0MDBa +Fw0zMTA0MjQxNzE0MDBaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y +bmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKEwtCbG9ja3N0cmVh +bTEdMBsGA1UECxMUQ2VydGlmaWNhdGVBdXRob3JpdHkxDTALBgNVBAMTBEdMIC8w +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATp83k4SqQ5geGRpIpDuU20vrZz8qJ8 +eBDYbW3nIlC8UM/PzVBSNA/MqWlAamB3YGK+VlgsEMbeOUWEM4c9ztVlo2QwYjAO +BgNVHQ8BAf8EBAMCAaYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIG +A1UdEwEB/wQIMAYBAf8CAQMwHQYDVR0OBBYEFM6ha+o75cd25WbWGqXGR1WKTikj +MAoGCCqGSM49BAMCA0gAMEUCIGBkjyp1Nd/m/b3jEAUmxAisqCahuQUPuyQrIwo0 +ZF/9AiEAsZ8qZfkUZH2Ya7y6ccFTDps/ahsFWSrRao8ru3yhhrs= +-----END CERTIFICATE----- diff --git a/examples/python/getting-started/main.py b/examples/python/getting-started/main.py new file mode 100644 index 000000000..06cdd94a7 --- /dev/null +++ b/examples/python/getting-started/main.py @@ -0,0 +1,120 @@ +import bip39 +from glclient import Credentials, Signer, Scheduler +from pathlib import Path +from pyln import grpc as clnpb +import secrets # Make sure to use cryptographically sound randomness + + +def save_to_file(file_name: str, data: bytes) -> None: + with open(file_name, "wb") as file: + file.write(data) + + +def read_file(file_name: str) -> bytes: + with open(file_name, "rb") as file: + return file.read() + + +def create_seed(): + # ---8<--- [start: create_seed] + rand = secrets.randbits(256).to_bytes(32, "big") # 32 bytes of randomness + + # Show seed phrase to user + phrase = bip39.encode_bytes(rand) + + seed = bip39.phrase_to_seed(phrase)[:32] # Only need the first 32 bytes + + # Store the seed on the filesystem, or secure configuration system + save_to_file("seed", seed) + + # ---8<--- [end: create_seed] + return seed + + +def nobody_with_identity(developer_cert, developer_key): + ca = Path("ca.pem").open(mode="rb").read() + return Credentials.nobody_with(developer_cert, developer_key, ca) + + +Credentials.nobody_with_identity = nobody_with_identity + + +# Validated against gl-testing +def register_node(seed: bytes, developer_cert_path: str, developer_key_path: str): + # ---8<--- [start: dev_creds] + developer_cert = Path(developer_cert_path).open(mode="rb").read() + developer_key = Path(developer_key_path).open(mode="rb").read() + + developer_creds = Credentials.nobody_with_identity(developer_cert, developer_key) + # ---8<--- [end: dev_creds] + + # ---8<--- [start: init_signer] + network = "bitcoin" + signer = Signer(seed, network, developer_creds) + # ---8<--- [end: init_signer] + + # ---8<--- [start: register_node] + scheduler = Scheduler(signer.node_id(), network, developer_creds) + + # Passing in the signer is required because the client needs to prove + # ownership of the `node_id` + registration_response = scheduler.register(signer, invite_code=None) + + device_creds = Credentials.from_bytes(registration_response.creds) + # save_to_file("creds", device_creds.to_bytes()); + # ---8<--- [end: register_node] + + # ---8<--- [start: get_node] + scheduler = scheduler.authenticate(device_creds) + node = scheduler.node() + # ---8<--- [end: get_node] + + +# TODO: Remove node_id from signature and add node_id to credentials +def start_node(device_creds_path: str, node_id: bytes): + # ---8<--- [start: start_node] + network = "bitcoin" + device_creds = Credentials.from_path(device_creds_path) + scheduler = Scheduler(node_id, network, device_creds) + + node = scheduler.node() + # ---8<--- [end: start_node] + + # ---8<--- [start: list_peers] + info = node.get_info() + peers = node.list_peers() + # ---8<--- [end: list_peers] + + # ---8<--- [start: start_signer] + seed = read_file("seed") + signer = Signer(seed, network, device_creds) + + signer.run_in_thread() + # ---8<--- [end: start_signer] + + # ---8<--- [start: create_invoice] + node.invoice( + amount_msat=clnpb.AmountOrAny(amount=clnpb.Amount(msat=10000)), + description="description", + label="label", + ) + # ---8<--- [end: create_invoice] + + +def recover_node(device_cert: bytes, device_key: bytes): + # ---8<--- [start: recover_node] + seed = read_file("seed") + network = "bitcoin" + signer_creds = Credentials.with_identity(device_cert, device_key) + signer = Signer(seed, network, signer_creds) + + scheduler_creds = signer_creds.upgrade(signer, scheduler) + + scheduler = Scheduler( + signer.node_id(), + network, + scheduler_creds, + ) + + scheduler.recover(signer) + # ---8<--- [end: recover_node] diff --git a/examples/rust/getting-started/src/main.rs b/examples/rust/getting-started/src/main.rs index f6b15af96..70b6e0b76 100644 --- a/examples/rust/getting-started/src/main.rs +++ b/examples/rust/getting-started/src/main.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Result}; use bip39::{Language, Mnemonic}; use gl_client::credentials::{Device, Nobody, RuneProvider, TlsConfigProvider}; use gl_client::node::ClnClient; +use gl_client::pb::cln::{amount_or_any, Amount, AmountOrAny}; use gl_client::pb::{self, cln}; use gl_client::scheduler::Scheduler; use gl_client::{bitcoin::Network, signer::Signer}; @@ -20,7 +21,7 @@ fn save_to_file(file_name: &str, data: Vec) { fs::write(file_name, data).unwrap(); } -fn read_file(file_name: &str) -> Vec{ +fn read_file(file_name: &str) -> Vec { fs::read(file_name).unwrap() } @@ -75,10 +76,11 @@ async fn register_node(seed: Vec, developer_cert_path: String, developer_key async fn start_node(device_creds_path: String) { // ---8<--- [start: start_node] + let network = Network::Bitcoin; let device_creds = Device::from_path(device_creds_path); let scheduler = gl_client::scheduler::Scheduler::new( device_creds.node_id().unwrap(), - gl_client::bitcoin::Network::Bitcoin, + network, device_creds.clone(), ) .await @@ -96,7 +98,6 @@ async fn start_node(device_creds_path: String) { // ---8<--- [end: list_peers] // ---8<--- [start: start_signer] - let network = Network::Bitcoin; let seed = read_file("seed"); let signer = Signer::new(seed, network, device_creds.clone()).unwrap(); @@ -107,9 +108,14 @@ async fn start_node(device_creds_path: String) { // ---8<--- [end: start_signer] // ---8<--- [start: create_invoice] + let amount = AmountOrAny { + value: Some(amount_or_any::Value::Amount(Amount { msat: 10000 })), + }; + node.invoice(cln::InvoiceRequest { - label: "label".to_string(), + amount_msat: Some(amount), description: "description".to_string(), + label: "label".to_string(), ..Default::default() }) .await @@ -130,7 +136,7 @@ async fn recover_node( let scheduler_creds = signer .add_base_rune_to_device_credentials(signer_creds) .unwrap(); - + let scheduler = gl_client::scheduler::Scheduler::new( signer.node_id(), gl_client::bitcoin::Network::Bitcoin,