diff --git a/bolt-cli/build.rs b/bolt-cli/build.rs index 1028e01e7..07c7d4466 100644 --- a/bolt-cli/build.rs +++ b/bolt-cli/build.rs @@ -10,7 +10,7 @@ fn main() -> io::Result<()> { } tonic_build::configure().build_client(true).out_dir(PB_OUT_DIR).compile_protos( - &["proto/eth2-signer-api/v1/lister.proto"], + &["proto/eth2-signer-api/v1/lister.proto", "proto/eth2-signer-api/v1/signer.proto"], &["proto/eth2-signer-api/v1/", "proto/eth2-signer-api/"], ) } diff --git a/bolt-cli/src/pb/mod.rs b/bolt-cli/src/pb/mod.rs index 1522b52ff..435dccf78 100644 --- a/bolt-cli/src/pb/mod.rs +++ b/bolt-cli/src/pb/mod.rs @@ -1,7 +1,12 @@ -pub mod v1; +mod v1; -#[allow(unused_imports)] -pub use v1::{ - lister_client::ListerClient, Account, DistributedAccount, ListAccountsRequest, - ListAccountsResponse, ResponseState, -}; +/// Re-exported protobuf API for the ETH2 remote signer service. +pub mod eth2_signer_api { + + #[allow(unused_imports)] + pub use super::v1::{ + lister_client::ListerClient, sign_request::Id as SignRequestId, + signer_client::SignerClient, Account, DistributedAccount, ListAccountsRequest, + ListAccountsResponse, ResponseState, SignRequest, SignResponse, + }; +} diff --git a/bolt-cli/src/pb/v1.rs b/bolt-cli/src/pb/v1.rs index 673b29588..a4e1e7a81 100644 --- a/bolt-cli/src/pb/v1.rs +++ b/bolt-cli/src/pb/v1.rs @@ -376,3 +376,693 @@ pub mod lister_server { const NAME: &'static str = SERVICE_NAME; } } +/// AttestationData is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AttestationData { + #[prost(uint64, tag = "1")] + pub slot: u64, + #[prost(uint64, tag = "2")] + pub committee_index: u64, + #[prost(bytes = "vec", tag = "3")] + pub beacon_block_root: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub source: ::core::option::Option, + #[prost(message, optional, tag = "5")] + pub target: ::core::option::Option, +} +/// Checkpoint is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Checkpoint { + #[prost(uint64, tag = "1")] + pub epoch: u64, + #[prost(bytes = "vec", tag = "2")] + pub root: ::prost::alloc::vec::Vec, +} +/// BeaconBlockheader is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BeaconBlockHeader { + #[prost(uint64, tag = "1")] + pub slot: u64, + #[prost(uint64, tag = "2")] + pub proposer_index: u64, + #[prost(bytes = "vec", tag = "3")] + pub parent_root: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "4")] + pub state_root: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "5")] + pub body_root: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignRequest { + #[prost(bytes = "vec", tag = "3")] + pub data: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "4")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(oneof = "sign_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignRequest`. +pub mod sign_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MultisignRequest { + #[prost(message, repeated, tag = "1")] + pub requests: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconAttestationRequest { + #[prost(bytes = "vec", tag = "3")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub data: ::core::option::Option, + #[prost(oneof = "sign_beacon_attestation_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignBeaconAttestationRequest`. +pub mod sign_beacon_attestation_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconAttestationsRequest { + #[prost(message, repeated, tag = "1")] + pub requests: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconProposalRequest { + #[prost(bytes = "vec", tag = "3")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub data: ::core::option::Option, + #[prost(oneof = "sign_beacon_proposal_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignBeaconProposalRequest`. +pub mod sign_beacon_proposal_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, + #[prost(bytes = "vec", tag = "2")] + pub signature: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MultisignResponse { + #[prost(message, repeated, tag = "1")] + pub responses: ::prost::alloc::vec::Vec, +} +/// Generated client implementations. +pub mod signer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct SignerClient { + inner: tonic::client::Grpc, + } + impl SignerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl SignerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> SignerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + SignerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn sign( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/Sign"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "Sign")); + self.inner.unary(req, path, codec).await + } + pub async fn multisign( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/Multisign"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "Multisign")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_attestation( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconAttestation", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestation")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_attestations( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconAttestations", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestations")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_proposal( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconProposal", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconProposal")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod signer_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with SignerServer. + #[async_trait] + pub trait Signer: std::marker::Send + std::marker::Sync + 'static { + async fn sign( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn multisign( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn sign_beacon_attestation( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn sign_beacon_attestations( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn sign_beacon_proposal( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct SignerServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl SignerServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for SignerServer + where + T: Signer, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/v1.Signer/Sign" => { + #[allow(non_camel_case_types)] + struct SignSvc(pub Arc); + impl tonic::server::UnaryService + for SignSvc { + type Response = super::SignResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sign(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SignSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/v1.Signer/Multisign" => { + #[allow(non_camel_case_types)] + struct MultisignSvc(pub Arc); + impl tonic::server::UnaryService + for MultisignSvc { + type Response = super::MultisignResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::multisign(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = MultisignSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/v1.Signer/SignBeaconAttestation" => { + #[allow(non_camel_case_types)] + struct SignBeaconAttestationSvc(pub Arc); + impl< + T: Signer, + > tonic::server::UnaryService + for SignBeaconAttestationSvc { + type Response = super::SignResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sign_beacon_attestation(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SignBeaconAttestationSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/v1.Signer/SignBeaconAttestations" => { + #[allow(non_camel_case_types)] + struct SignBeaconAttestationsSvc(pub Arc); + impl< + T: Signer, + > tonic::server::UnaryService + for SignBeaconAttestationsSvc { + type Response = super::MultisignResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sign_beacon_attestations(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SignBeaconAttestationsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/v1.Signer/SignBeaconProposal" => { + #[allow(non_camel_case_types)] + struct SignBeaconProposalSvc(pub Arc); + impl< + T: Signer, + > tonic::server::UnaryService + for SignBeaconProposalSvc { + type Response = super::SignResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sign_beacon_proposal(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SignBeaconProposalSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new(empty_body()); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for SignerServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "v1.Signer"; + impl tonic::server::NamedService for SignerServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/bolt-cli/src/pubkeys.rs b/bolt-cli/src/pubkeys.rs index 78ee7e962..c06eef8de 100644 --- a/bolt-cli/src/pubkeys.rs +++ b/bolt-cli/src/pubkeys.rs @@ -3,7 +3,7 @@ use eyre::Result; use lighthouse_eth2_keystore::Keystore; use crate::{ - pb::Account, + pb::eth2_signer_api::Account, utils::keystore::{keystore_paths, KeystoreError, KeystoreSecret}, }; diff --git a/bolt-cli/src/utils/dirk.rs b/bolt-cli/src/utils/dirk.rs index f93048879..52f38df9c 100644 --- a/bolt-cli/src/utils/dirk.rs +++ b/bolt-cli/src/utils/dirk.rs @@ -1,11 +1,15 @@ use std::fs; +use alloy_primitives::B256; +use ethereum_consensus::crypto::bls::{PublicKey as BlsPublicKey, Signature as BlsSignature}; use eyre::{Context, Result}; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use crate::{ cli::TlsCredentials, - pb::{Account, ListAccountsRequest, ListerClient}, + pb::eth2_signer_api::{ + Account, ListAccountsRequest, ListerClient, SignRequest, SignRequestId, SignerClient, + }, }; /// A Dirk remote signer. @@ -33,6 +37,28 @@ impl Dirk { Ok(accs.into_inner().accounts) } + + /// Request a signature from the remote signer. + pub async fn request_signature( + &self, + pubkey: BlsPublicKey, + hash: B256, + domain: B256, + ) -> Result { + let mut signer = SignerClient::new(self.conn.clone()); + + let req = SignRequest { + data: hash.to_vec(), + domain: domain.to_vec(), + id: Some(SignRequestId::PublicKey(pubkey.to_vec())), + }; + + let res = signer.sign(req).await?; + let sig = res.into_inner().signature; + let sig = BlsSignature::try_from(sig.as_slice()).wrap_err("Failed to parse signature")?; + + Ok(sig) + } } /// Compose the TLS credentials from the given paths.