Skip to content

Commit

Permalink
Added configure method to greenlight proto file, regenerated grpc fil…
Browse files Browse the repository at this point in the history
…es, and added python bindings.
  • Loading branch information
Randy808 committed Sep 25, 2023
1 parent 9affaa8 commit 53d59ad
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 25 deletions.
6 changes: 6 additions & 0 deletions libs/gl-client-py/glclient/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,12 @@ def get_lsp_client(
native_lsps = self.inner.get_lsp_client()
return LspClient(native_lsps, peer_id)

def configure(self, close_to_addr: str) -> None:
req = nodepb.GlConfig(
close_to_addr=close_to_addr
).SerializeToString()

return self.inner.configure(bytes(req))

def normalize_node_id(node_id, string=False):
if len(node_id) == 66:
Expand Down
52 changes: 27 additions & 25 deletions libs/gl-client-py/glclient/greenlight_pb2.py

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions libs/gl-client-py/glclient/greenlight_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,21 @@ class NodeConfig(google.protobuf.message.Message):

global___NodeConfig = NodeConfig

@typing_extensions.final
class GlConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

CLOSE_TO_ADDR_FIELD_NUMBER: builtins.int
close_to_addr: builtins.str
def __init__(
self,
*,
close_to_addr: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["close_to_addr", b"close_to_addr"]) -> None: ...

global___GlConfig = GlConfig

@typing_extensions.final
class StartupMessage(google.protobuf.message.Message):
"""A message that we know will be requested by `lightningd` at
Expand Down
33 changes: 33 additions & 0 deletions libs/gl-client-py/glclient/greenlight_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ def __init__(self, channel):
request_serializer=greenlight__pb2.HsmResponse.SerializeToString,
response_deserializer=greenlight__pb2.Empty.FromString,
)
self.Configure = channel.unary_unary(
'/greenlight.Node/Configure',
request_serializer=greenlight__pb2.GlConfig.SerializeToString,
response_deserializer=greenlight__pb2.Empty.FromString,
)


class NodeServicer(object):
Expand Down Expand Up @@ -329,6 +334,12 @@ def RespondHsmRequest(self, request, context):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def Configure(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_NodeServicer_to_server(servicer, server):
rpc_method_handlers = {
Expand Down Expand Up @@ -432,6 +443,11 @@ def add_NodeServicer_to_server(servicer, server):
request_deserializer=greenlight__pb2.HsmResponse.FromString,
response_serializer=greenlight__pb2.Empty.SerializeToString,
),
'Configure': grpc.unary_unary_rpc_method_handler(
servicer.Configure,
request_deserializer=greenlight__pb2.GlConfig.FromString,
response_serializer=greenlight__pb2.Empty.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'greenlight.Node', rpc_method_handlers)
Expand Down Expand Up @@ -796,6 +812,23 @@ def RespondHsmRequest(request,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def Configure(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/greenlight.Node/Configure',
greenlight__pb2.GlConfig.SerializeToString,
greenlight__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)


class HsmStub(object):
"""Missing associated documentation comment in .proto file."""
Expand Down
10 changes: 10 additions & 0 deletions libs/gl-client-py/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ impl Node {
self.cln_client.clone()
)
}

fn configure(&self, payload: &[u8]) -> PyResult<()> {
let req = pb::GlConfig::decode(payload).map_err(error_decoding_request)?;

exec(self.client.clone().configure(req))
.map(|x| x.into_inner())
.map_err(error_calling_remote_method)?;

return Ok(());
}
}

fn error_decoding_request<D: core::fmt::Display>(e: D) -> PyErr {
Expand Down
31 changes: 31 additions & 0 deletions libs/gl-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub async fn init(
.hook("htlc_accepted", lsp::on_htlc_accepted)
.hook("invoice_payment", on_invoice_payment)
.hook("peer_connected", on_peer_connected)
.hook("openchannel", on_openchannel)
.hook("custommsg", on_custommsg);
Ok(Builder {
state,
Expand Down Expand Up @@ -154,6 +155,36 @@ async fn on_peer_connected(plugin: Plugin, v: serde_json::Value) -> Result<serde
Ok(json!({"result": "continue"}))
}

async fn on_openchannel(plugin: Plugin, v: serde_json::Value) -> Result<serde_json::Value> {
debug!("Received an openchannel request: {:?}", v);
let mut rpc = cln_rpc::ClnRpc::new(plugin.configuration().rpc_file).await?;

let req = cln_rpc::model::requests::ListdatastoreRequest{
key: Some(vec![
"glconf".to_string(),
"channel".to_string(),
"close_to_addr".to_string(),
])
};

let res = rpc.call_typed(req).await;
debug!("openchannel response: {:?}", res);

match res {
Ok(r) => {
if r.datastore.is_empty() {
return Ok(json!({"result": "continue"}))
}

Ok(json!({"result": "continue", "close_to": r.datastore[0].string}))
},
Err(e) => {
log::debug!("An error occurred while searching for a custom close_to address: {}", e);
Ok(json!({"result": "continue"}))
}
}
}

/// Notification handler that receives notifications on incoming
/// payments, then looks up the invoice in the DB, and forwards the
/// full information to the GRPC interface.
Expand Down
63 changes: 63 additions & 0 deletions libs/gl-plugin/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use tokio_stream::wrappers::ReceiverStream;
use tonic::{transport::ServerTlsConfig, Code, Request, Response, Status};
mod wrapper;
pub use wrapper::WrappedNodeServer;
use::gl_client::bitcoin;
use std::str::FromStr;

static LIMITER: OnceCell<RateLimiter<NotKeyed, InMemoryState, MonotonicClock>> =
OnceCell::const_new();
Expand Down Expand Up @@ -880,6 +882,67 @@ impl Node for PluginNodeServer {
Err(e) => Err(Status::new(Code::Internal, e.to_string())),
}
}

async fn configure(&self, req: tonic::Request<pb::GlConfig>) -> Result<Response<pb::Empty>, Status> {
self.limit().await;
let gl_config = req.into_inner();
let rpc = self.get_rpc().await;

let res: Result<crate::responses::GetInfo, crate::rpc::Error> =
rpc.call("getinfo", json!({})).await;

let network = match res {
Ok(get_info_response) => {
match get_info_response.network.parse() {
Ok(v) => v,
Err(_) => Err(Status::new(Code::Unknown, format!("The network returned from the call to 'getinfo' could not be parsed")))?,
}
}
Err(e) => {
return Err(Status::new(
Code::Unknown,
format!("Failed to retrieve a response from 'getinfo' while setting the node's configuration: {}", e),
));
}
};

match bitcoin::Address::from_str(&gl_config.close_to_addr) {
Ok(address) => {
if address.network != network {
return Err(Status::new(
Code::Unknown,
format!(
"Network mismatch: \
Expected an address for {} but received an address for {}",
network,
address.network
),
));
}
}
Err(e) => {
return Err(Status::new(
Code::Unknown,
format!("The address {} is not valid: {}", gl_config.close_to_addr, e),
));
}
}

let res: Result<crate::responses::DatastoreResponse, crate::rpc::Error> =
rpc.call("datastore", json!({
"key": vec![
"glconf".to_string(),
"channel".to_string(),
"close_to_addr".to_string()
],
"string": gl_config.close_to_addr,
})).await;

match res {
Ok(_) => Ok(Response::new(pb::Empty::default())),
Err(e) => Err(Status::new(Code::Unknown, e.to_string())),
}
}
}

use cln_grpc::pb::node_server::NodeServer;
Expand Down
8 changes: 8 additions & 0 deletions libs/gl-plugin/src/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,14 @@ pub struct ListFunds {
pub channels: Vec<ListFundsChannel>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct DatastoreResponse {
pub key: Vec<String>,
pub generation: Option<u64>,
pub hex: Option<String>,
pub string: Option<String>,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Empty file modified libs/gl-testing/.gitignore
100644 → 100755
Empty file.
23 changes: 23 additions & 0 deletions libs/gl-testing/tests/test_gl_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,26 @@ def check(gl1):
return len(res.peers) == 1 and res.peers[0].connected

wait_for(lambda: check(gl1))

def test_configure_close_to_addr(node_factory, clients, bitcoind):
l1, l2 = node_factory.line_graph(2)
l2.fundwallet(sats=2*10**6)

c = clients.new()
c.register(configure=True)
gl1 = c.node()

s = c.signer().run_in_thread()
gl1.connect_peer(l2.info['id'], f'127.0.0.1:{l2.daemon.port}')

close_to_addr = bitcoind.getnewaddress()
gl1.configure(close_to_addr)

l2.rpc.fundchannel(c.node_id.hex(), 'all')
bitcoind.generate_block(1, wait_for_mempool=1)

wait_for(lambda:
gl1.list_peers().peers[0].channels[0].state == 'CHANNELD_NORMAL'
)

assert gl1.list_peers().peers[0].channels[0].close_to_addr == close_to_addr
6 changes: 6 additions & 0 deletions libs/proto/greenlight.proto
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ service Node {

rpc RespondHsmRequest(HsmResponse) returns (Empty) {}

rpc Configure(GlConfig) returns (Empty) {}

}

message HsmRequestContext {
Expand Down Expand Up @@ -605,6 +607,10 @@ message NodeConfig {
repeated StartupMessage startupmsgs = 1;
}

message GlConfig {
string close_to_addr = 1;
}

// A message that we know will be requested by `lightningd` at
// startup, and that we stash a response to on the scheduler. This
// allows the scheduler to start a node without requiring the signer
Expand Down

0 comments on commit 53d59ad

Please sign in to comment.