-
Notifications
You must be signed in to change notification settings - Fork 913
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
base: master
Are you sure you want to change the base?
Conversation
82996a7
to
0663de9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
3579c8d
to
dfecf91
Compare
Nice! Might take a while to reach feature parity, I've asked @ShahanaFarooqui to review... |
There was a problem hiding this 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
server_params | ||
.distinguished_name | ||
.push(rcgen::DnType::CommonName, "cln rest server"); | ||
if let Ok(ip) = rest_host.parse::<IpAddr>() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(), | ||
)?; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@@ -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): |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
||
|
||
def test_clnrest_websocket_upgrade_header(node_factory): | ||
"""Test that not setting an upgrade header leads to rejection""" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
f77ec03
to
1b04270
Compare
Thanks for the quick feedback @niftynei ! If you are new to rust i can recommend https://doc.rust-lang.org/book/ |
This comment was marked as resolved.
This comment was marked as resolved.
1b04270
to
6c4bea0
Compare
Built ontop of #7544 after getting merged on master. Fixes compatibility with multi options. |
6c4bea0
to
956abee
Compare
as a nixos user, i am very interested in this move away from python! |
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. |
d9eb409
to
6af4816
Compare
I've done it like this:
Also rebased on master and fixed tests for Valgrind (had to wait for stuff longer because Valgrind is so slow). |
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? |
Oh actually you can reference the runes by their index instead of their actual value, that might be the way to go? |
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
6af4816
to
855bce6
Compare
I've replaced the actual rune with the rune id (at the cost of an extra rpc call to
|
This plugin has been added to https://github.com/lightningd/plugins#available-plugins if anyone wants to try it out! See "clnrest-rs" |
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 likepip 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