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.

Added functionality to allow the last configure request to be sent to the signer on every signing request.
  • Loading branch information
Randy808 committed Nov 1, 2023
1 parent 008844c commit e0d0b1d
Show file tree
Hide file tree
Showing 17 changed files with 3,521 additions and 589 deletions.
3 changes: 3 additions & 0 deletions libs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions libs/gl-client-py/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PYDIR=${REPO}/libs/gl-client-py
PROTODIR=${REPO}/libs/proto

PYPROTOC_OPTS = \
-I${PROTODIR} \
-I ${PROTODIR} \
--python_out=${PYDIR}/glclient \
--grpc_python_out=${PYDIR}/glclient \
--experimental_allow_proto3_optional \
Expand All @@ -30,9 +30,7 @@ PROTOSRC = \

GENALL += ${PYPROTOS}

pygrpc: ${PYPROTOS}

${PYPROTOS}: ${PROTOSRC}
pygrpc: ${PROTOSRC}
python -m grpc_tools.protoc ${PYPROTOC_OPTS} scheduler.proto
python -m grpc_tools.protoc ${PYPROTOC_OPTS} greenlight.proto
sed -i 's/import scheduler_pb2 as scheduler__pb2/from . import scheduler_pb2 as scheduler__pb2/g' ${PYDIR}/glclient/scheduler_pb2_grpc.py
Expand Down
6 changes: 6 additions & 0 deletions libs/gl-client-py/glclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,12 @@ def get_lsp_client(
native_lsps = self.inner.get_lsp_client()
return LspClient(native_lsps)

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
78 changes: 27 additions & 51 deletions libs/gl-client-py/glclient/greenlight_pb2.py

Large diffs are not rendered by default.

21 changes: 21 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,27 @@ class NodeConfig(google.protobuf.message.Message):

global___NodeConfig = NodeConfig

@typing_extensions.final
class GlConfig(google.protobuf.message.Message):
"""The `GlConfig` is used to pass greenlight-specific startup parameters
to the node. The `gl-plugin` will look for a serialized config object in
the node's datastore to load these values from. Please refer to the
individual fields to learn what they do.
"""

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
564 changes: 33 additions & 531 deletions libs/gl-client-py/glclient/greenlight_pb2_grpc.py

Large diffs are not rendered by default.

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
20 changes: 20 additions & 0 deletions libs/gl-client/src/signer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use vls_protocol::serde_bolt::Octets;
use vls_protocol_signer::approver::{Approve, MemoApprover};
use vls_protocol_signer::handler;
use vls_protocol_signer::handler::Handler;
use lightning_signer::bitcoin::secp256k1::PublicKey;

mod approver;
mod auth;
Expand Down Expand Up @@ -376,6 +377,25 @@ impl Signer {
return Err(Error::Resolver(req.raw, ctxrequests));
};

// If present, add the close_to_addr to the allowlist
for parsed_request in ctxrequests.iter() {
match parsed_request {
model::Request::GlConfig(gl_config) => {
let pubkey = PublicKey::from_slice(&self.id);
match pubkey {
Ok(p) => {
let _ = self
.services
.persister
.update_node_allowlist(&p, vec![gl_config.close_to_addr.clone()]);
}
Err(e) => debug!("Could not parse public key {:?}: {:?}", self.id, e),
}
}
_ => {}
}
}

use auth::Authorizer;
let auth = auth::GreenlightAuthorizer {};
let approvals = auth.authorize(ctxrequests).map_err(|e| Error::Auth(e))?;
Expand Down
3 changes: 3 additions & 0 deletions libs/gl-client/src/signer/model/greenlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub fn decode_request(uri: &str, p: &[u8]) -> anyhow::Result<Request> {
"/greenlight.Node/ConnectPeer" => {
Request::GlConnectPeer(crate::pb::ConnectRequest::decode(p)?)
}
"/greenlight.Node/Configure" => {
Request::GlConfig(crate::pb::GlConfig::decode(p)?)
}
uri => return Err(anyhow!("Unknown URI {}, can't decode payload", uri)),
})
}
1 change: 1 addition & 0 deletions libs/gl-client/src/signer/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum Request {
GlListPayments(greenlight::ListPaymentsRequest),
GlListInvoices(greenlight::ListInvoicesRequest),
GlConnectPeer(greenlight::ConnectRequest),
GlConfig(greenlight::GlConfig),
Getinfo(cln::GetinfoRequest),
ListPeers(cln::ListpeersRequest),
ListFunds(cln::ListfundsRequest),
Expand Down
2 changes: 1 addition & 1 deletion libs/gl-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ path = "src/bin/plugin.rs"
[dependencies]
anyhow = "*"
async-stream = "*"
bytes = "1"
bytes = { version = "1", features = ["serde"] }
clightningrpc = "*"
cln-grpc = {git = "https://github.com/ElementsProject/lightning.git", branch = "master", features = ["server"] }
cln-rpc = {git = "https://github.com/ElementsProject/lightning.git", branch = "master"}
Expand Down
3 changes: 2 additions & 1 deletion libs/gl-plugin/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use serde::{Serialize, Deserialize};

#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Request {
// The caller's mTLS public key
pubkey: Vec<u8>,
Expand Down
61 changes: 61 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,66 @@ 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(),
"request".to_string(),
])
};

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

match res {
Ok(res) => {
if !res.datastore.is_empty() {
match &res.datastore[0].string {
Some(serialized_request) => {
match _parse_gl_config_from_serialized_request(serialized_request.to_string()) {
Some(gl_config) => {
return Ok(json!({"result": "continue", "close_to": gl_config.close_to_addr}));
}
None => {
debug!("Failed to parse the GlConfig from the serialized request's payload");
}
}
}
None => {
debug!("Got empty response from datastore for key glconf.request");
}
}
}

return Ok(json!({"result": "continue"}))
}
Err(e) => {
log::debug!("An error occurred while searching for a custom close_to address: {}", e);
Ok(json!({"result": "continue"}))
}
}
}

fn _parse_gl_config_from_serialized_request(request: String) -> Option<pb::GlConfig> {
use prost::Message;
let gl_conf_req: crate::context::Request = serde_json::from_str(&request).unwrap();
let gl_conf_req: crate::pb::PendingRequest = gl_conf_req.into();
let payload = &gl_conf_req.request[5..];
let glconfig = crate::pb::GlConfig::decode(payload);

match glconfig {
Ok(glconfig) => Some(glconfig),
Err(e) => {
debug!("Failed to parse glconfig from string: {:?}", e);
None
}
}
}


/// 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
94 changes: 93 additions & 1 deletion libs/gl-plugin/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ 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 All @@ -36,6 +39,8 @@ lazy_static! {
/// initiate operations that might require signatures.
static ref SIGNER_COUNT: AtomicUsize = AtomicUsize::new(0);
static ref RPC_BCAST: broadcast::Sender<super::Event> = broadcast::channel(4).0;

static ref SERIALIZED_CONFIGURE_REQUEST: Mutex<Option<String>> = Mutex::new(None);
}

/// The PluginNodeServer is the interface that is exposed to client devices
Expand Down Expand Up @@ -254,7 +259,7 @@ impl Node for PluginNodeServer {
let mut stream = self.stage.mystream().await;
let signer_state = self.signer_state.clone();
let ctx = self.ctx.clone();

tokio::spawn(async move {
trace!("hsmd hsm_id={} request processor started", hsm_id);
loop {
Expand Down Expand Up @@ -289,6 +294,20 @@ impl Node for PluginNodeServer {

req.request.signer_state = state.into();
req.request.requests = ctx.snapshot().await.into_iter().map(|r| r.into()).collect();

let serialized_configure_request = SERIALIZED_CONFIGURE_REQUEST.lock().await;

match &(*serialized_configure_request) {
Some(serialized_configure_request) => {
let configure_request = serde_json::from_str::<crate::context::Request>(
serialized_configure_request,
)
.unwrap();
req.request.requests.push(configure_request.into());
}
None => {}
}

debug!(
"Sending signer requests with {} requests and {} state entries",
req.request.requests.len(),
Expand Down Expand Up @@ -387,6 +406,79 @@ impl Node for PluginNodeServer {

return Ok(Response::new(ReceiverStream::new(rx)));
}

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!("Failed to parse 'network' from 'getinfo' response"),
))?,
},
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 requests: Vec<crate::context::Request> = self.ctx.snapshot().await.into_iter().map(|r| r.into()).collect();
let serialized_req = serde_json::to_string(&requests[0]).unwrap();
let datastore_res: Result<crate::cln_rpc::model::responses::DatastoreResponse, crate::rpc::Error> =
rpc.call("datastore", json!({
"key": vec![
"glconf".to_string(),
"request".to_string(),
],
"string": serialized_req,
})).await;

match datastore_res {
Ok(_) => {
let mut cached_gl_config = SERIALIZED_CONFIGURE_REQUEST.lock().await;
*cached_gl_config = Some(serialized_req);

Ok(Response::new(pb::Empty::default()))
}
Err(e) => {
return Err(Status::new(
Code::Unknown,
format!("Failed to store the raw configure request in the datastore: {}", e),
))
}
}
}
}

use cln_grpc::pb::node_server::NodeServer;
Expand Down
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 @@ -123,3 +123,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 == 2
)

assert gl1.list_peers().peers[0].channels[0].close_to_addr == close_to_addr
Loading

0 comments on commit e0d0b1d

Please sign in to comment.