Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clnrest: replace with rust plugin #7509

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

daywalker90
Copy link
Contributor

This is intended to be a drop-in replacement for @ShahanaFarooqui's clnrest.py. Python dependency management, especially in the context of CLN is not easy. Quite a few people struggled to setup CLN + python dependencies on modern unix OS. E.g. on debian 12 you have great choices like pip install --user --break-system-packages -r requirements.txt (you are only running CLN with this user right?) or hard-coding the PATH and PYTHON_PATH variables in your systemd service.

Installing a binary is easier, so @chrisguida bribed me into rewriting clnrest.py in rust.

In a future PR i intend to support creating dynamic REST routes for other plugins using @gudnuf PR's #7507 and #7508

@daywalker90 daywalker90 force-pushed the clnrest-rs branch 2 times, most recently from 82996a7 to 0663de9 Compare July 31, 2024 13:10
Copy link
Collaborator

@vincenzopalazzo vincenzopalazzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

@rustyrussell
Copy link
Contributor

Nice! Might take a while to reach feature parity, I've asked @ShahanaFarooqui to review...

Copy link
Collaborator

@niftynei niftynei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some early feedback, based on looking through the tests and running startup_regtest locally. still need to go through the rust code

i had two patches to make startup_regtest work, see daywalker90#6

plugins/clnrest-plugin/src/certs.rs Outdated Show resolved Hide resolved
server_params
.distinguished_name
.push(rcgen::DnType::CommonName, "cln rest server");
if let Ok(ip) = rest_host.parse::<IpAddr>() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm pretty new to rust so apologies if this is obvious, but what happens in the case where the rest_host string fails to parse as an IpAddr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the code block after will not execute. if let Ok(ip) means "get the Ok type into ip". I could do something with the error by appending a else block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if let syntax here:

if let Ok(something) = some_operation() {
  println!("Hello {}", something)
} else {}

is equivalent to

match some_operation() {
  Ok(something) => println!("Hello {}", something),
  Err(_e) => {}
}

NOtice that the match statement allows us to extract the error, which I'm not sure we'd do in the if let case.

let ca_key = KeyPair::generate()?;
let ca_cert = ca_params.self_signed(&ca_key)?;

fs::create_dir_all(certs_path)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if this fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ? will propagate the error and in this case right to the main function where there is another ? after generate_certificates which will stop the plugin with the error message from fs::create_dir_all. When does this error:

"This function will return an error in the following situations, but is not limited to just these cases:

If any directory in the path specified by path does not already exist and it could not be created otherwise. The specific error conditions for when a directory is being created (after it is determined to not exist) are outlined by [fs::create_dir].
Notable exception is made for situations where any of the directories specified in the path could not be created as it was being created concurrently. Such cases are considered to be successful. That is, calling create_dir_all concurrently from multiple threads or processes is guaranteed not to fail due to a race condition with itself."

fs::write(
certs_path.join("client-key.pem"),
client_key.serialize_pem().as_bytes(),
)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if this fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ? at the end of a function call that can error means that the error wil propagate up. In this case directly to the main function and the plugin would stop with the error message generated by fs::write

tests/test_clnrest.py Outdated Show resolved Hide resolved
@@ -251,7 +251,7 @@ def test_clnrest_large_response(node_factory):
# to complain with the errors F811 like this "F811 redefinition of
# unused 'message'".

def notifications_received_via_websocket(l1, base_url, http_session, rpc_method='invoice', rpc_params=[100000, 'label', 'description']):
def notifications_received_via_websocket(l1, base_url, http_session, rpc_method='invoice', rpc_params=[100000, 'label', 'description'], expect_error=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice addition!

@@ -390,10 +401,10 @@ def test_clnrest_numeric_msat_notification(node_factory):
rune_clnrest_notifications = l2.rpc.createrune(restrictions=[["method=listclnrest-notifications"]])['rune']
http_session.headers.update({"rune": rune_clnrest_notifications})
notifications = notifications_received_via_websocket(l1, base_url, http_session, 'pay', [inv['bolt11']])
filtered_notifications = [n for n in notifications if 'invoice_creation' in n]
filtered_notifications = [n for n in notifications if 'invoice_payment' in n]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the rationale for changing this notification keyword?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The python version of clnrest was only sending notifications every 1 second, while the rust version sends them immediately. In the tests it only starts listening to notifications when calling notifications_received_via_websocket so it would miss the notification from the invoice creation before while the python version would still catch it because of the queue.

tests/test_clnrest.py Outdated Show resolved Hide resolved


def test_clnrest_websocket_upgrade_header(node_factory):
"""Test that not setting an upgrade header leads to rejection"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it not possible to call the http endpoints w/o using a websocket? or is there a subset of RPCs that require websocket connections to work?

this seems like a change in behavior from the python client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment may be misleading i think. It is possible to call the http endpoints without using a websocket. In fact the websocket in clnrest is only for listening to notifications.

I wrote this test because in order to differentiate what a client wants when calling the root path i'm checking the upgrade header. Every websocket client i tested (RTL + ZEUS) included that but not the client used in the tests.

Not setting this header (as in this test) makes the request look like a regular http request and in this case will return a redirect to the swagger ui which the socketio client interprets as an unexpected response.

I'd say in practice this does not make a change in behaviour since a normal websocket client sets the upgrade header.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only wrote this tests because i had a bug in an earlier version where not setting the upgrade header would circumvent the rune check for notifications.

@daywalker90 daywalker90 force-pushed the clnrest-rs branch 2 times, most recently from f77ec03 to 1b04270 Compare August 8, 2024 09:44
@daywalker90
Copy link
Contributor Author

Thanks for the quick feedback @niftynei ! If you are new to rust i can recommend https://doc.rust-lang.org/book/

@daywalker90

This comment was marked as resolved.

@daywalker90
Copy link
Contributor Author

Built ontop of #7544 after getting merged on master. Fixes compatibility with multi options.

@niftynei
Copy link
Collaborator

as a nixos user, i am very interested in this move away from python!

@chrisguida
Copy link
Contributor

Feature request: log which runes were used for which calls. That way an organization can track which users authorized which transactions by giving different runes to different users.

@daywalker90 daywalker90 force-pushed the clnrest-rs branch 2 times, most recently from d9eb409 to 6af4816 Compare September 22, 2024 10:16
@daywalker90
Copy link
Contributor Author

daywalker90 commented Sep 22, 2024

Feature request: log which runes were used for which calls. That way an organization can track which users authorized which transactions by giving different runes to different users.

I've done it like this:

INFO    plugin-clnrest: Authorized `ysx3Jj5nOUWk3qujMHD8qo5Lcc0UlsXoDyXuhEC4_a49MA==` access to `invoice` with params: `{\"amount_msat\":1000,\"description\":\"owiegh\",\"label\":\"owigerjh\"}`

Also rebased on master and fixed tests for Valgrind (had to wait for stuff longer because Valgrind is so slow).

@chrisguida
Copy link
Contributor

Awesome!!! Hmm, I'm realizing that these will be sensitive to have in the logs... is there any utility to clean stuff like this out?

Or maybe we can put in the db instead?

@chrisguida
Copy link
Contributor

Oh actually you can reference the runes by their index instead of their actual value, that might be the way to go?

daywalker90 and others added 3 commits September 24, 2024 10:35
Changelog-Changed: clnrest is now a rust plugin
Just check for the presence of the clnrest binary now.

Also update the string to check to the actual REST startup string
If you re-run a node several times, the log file fills with info from
previous runs. To avoid looking at old logs, only parse the most recent
run's logs when looking for the magic CLN rest startup/deactivated
strings

Changelog-Fixed: startup-regtest.sh now only inspects the most recent run's logs for the active status of the clnrest plugin
@daywalker90
Copy link
Contributor Author

I've replaced the actual rune with the rune id (at the cost of an extra rpc call to showrunes) and formatted the string a bit better for people who want to parse it:

"Authorized rune_id:`{}` access to method:`{}` with params:`{}`"

@chrisguida
Copy link
Contributor

This plugin has been added to https://github.com/lightningd/plugins#available-plugins if anyone wants to try it out!

See "clnrest-rs"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants