Skip to content

Commit c5c7476

Browse files
committed
Web3Signer support for VC (#2522)
[EIP-3030]: https://eips.ethereum.org/EIPS/eip-3030 [Web3Signer]: https://consensys.github.io/web3signer/web3signer-eth2.html ## Issue Addressed Resolves #2498 ## Proposed Changes Allows the VC to call out to a [Web3Signer] remote signer to obtain signatures. ## Additional Info ### Making Signing Functions `async` To allow remote signing, I needed to make all the signing functions `async`. This caused a bit of noise where I had to convert iterators into `for` loops. In `duties_service.rs` there was a particularly tricky case where we couldn't hold a write-lock across an `await`, so I had to first take a read-lock, then grab a write-lock. ### Move Signing from Core Executor Whilst implementing this feature, I noticed that we signing was happening on the core tokio executor. I suspect this was causing the executor to temporarily lock and occasionally trigger some HTTP timeouts (and potentially SQL pool timeouts, but I can't verify this). Since moving all signing into blocking tokio tasks, I noticed a distinct drop in the "atttestations_http_get" metric on a Prater node: ![http_get_times](https://user-images.githubusercontent.com/6660660/132143737-82fd3836-2e7e-445b-a143-cb347783baad.png) I think this graph indicates that freeing the core executor allows the VC to operate more smoothly. ### Refactor TaskExecutor I noticed that the `TaskExecutor::spawn_blocking_handle` function would fail to spawn tasks if it were unable to obtain handles to some metrics (this can happen if the same metric is defined twice). It seemed that a more sensible approach would be to keep spawning tasks, but without metrics. To that end, I refactored the function so that it would still function without metrics. There are no other changes made. ## TODO - [x] Restructure to support multiple signing methods. - [x] Add calls to remote signer from VC. - [x] Documentation - [x] Test all endpoints - [x] Test HTTPS certificate - [x] Allow adding remote signer validators via the API - [x] Add Altair support via [21.8.1-rc1](https://github.com/ConsenSys/web3signer/releases/tag/21.8.1-rc1) - [x] Create issue to start using latest version of web3signer. (See #2570) ## Notes - ~~Web3Signer doesn't yet support the Altair fork for Prater. See Consensys/web3signer#423 - ~~There is not yet a release of Web3Signer which supports Altair blocks. See Consensys/web3signer#391
1 parent 58012f8 commit c5c7476

37 files changed

+2238
-480
lines changed

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ members = [
7676
"testing/node_test_rig",
7777
"testing/simulator",
7878
"testing/state_transition_vectors",
79+
"testing/web3signer_tests",
7980

8081
"validator_client",
8182
"validator_client/slashing_protection",

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* [Advanced Usage](./advanced.md)
3333
* [Custom Data Directories](./advanced-datadir.md)
3434
* [Validator Graffiti](./graffiti.md)
35+
* [Remote Signing with Web3Signer](./validator-web3signer.md)
3536
* [Database Configuration](./advanced_database.md)
3637
* [Advanced Networking](./advanced_networking.md)
3738
* [Running a Slasher](./slasher.md)

book/src/api-vc-endpoints.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,43 @@ Typical Responses | 200
434434
]
435435
}
436436
```
437+
438+
## `POST /lighthouse/validators/web3signer`
439+
440+
Create any number of new validators, all of which will refer to a
441+
[Web3Signer](https://docs.web3signer.consensys.net/en/latest/) server for signing.
442+
443+
### HTTP Specification
444+
445+
| Property | Specification |
446+
| --- |--- |
447+
Path | `/lighthouse/validators/web3signer`
448+
Method | POST
449+
Required Headers | [`Authorization`](./api-vc-auth-header.md)
450+
Typical Responses | 200, 400
451+
452+
### Example Request Body
453+
454+
```json
455+
[
456+
{
457+
"enable": true,
458+
"description": "validator_one",
459+
"graffiti": "Mr F was here",
460+
"voting_public_key": "0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380",
461+
"url": "http://path-to-web3signer.com",
462+
"root_certificate_path": "/path/on/vc/filesystem/to/certificate.pem",
463+
"request_timeout_ms": 12000
464+
}
465+
]
466+
```
467+
468+
The following fields may be omitted or nullified to obtain default values:
469+
470+
- `graffiti`
471+
- `root_certificate_path`
472+
- `request_timeout_ms`
473+
474+
### Example Response Body
475+
476+
*No data is included in the response body.*

book/src/validator-web3signer.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Remote Signing with Web3Signer
2+
3+
[Web3Signer]: https://docs.web3signer.consensys.net/en/latest/
4+
[Consensys]: https://github.com/ConsenSys/
5+
[Teku]: https://github.com/consensys/teku
6+
7+
[Web3Signer] is a tool by Consensys which allows *remote signing*. Remote signing is when a
8+
Validator Client (VC) out-sources the signing of messages to remote server (e.g., via HTTPS). This
9+
means that the VC does not hold the validator private keys.
10+
11+
## Warnings
12+
13+
Using a remote signer comes with risks, please read the following two warnings before proceeding:
14+
15+
### Remote signing is complex and risky
16+
17+
Remote signing is generally only desirable for enterprise users or users with unique security
18+
requirements. Most users will find the separation between the Beacon Node (BN) and VC to be
19+
sufficient *without* introducing a remote signer.
20+
21+
**Using a remote signer introduces a new set of security and slashing risks and should only be
22+
undertaken by advanced users who fully understand the risks.**
23+
24+
### Web3Signer is not maintained by Lighthouse
25+
26+
The [Web3Signer] tool is maintained by [Consensys], the same team that maintains [Teku]. The
27+
Lighthouse team (Sigma Prime) does not maintain Web3Signer or make any guarantees about its safety
28+
or effectiveness.
29+
30+
## Usage
31+
32+
A remote signing validator is added to Lighthouse in much the same way as one that uses a local
33+
keystore, via the [`validator_definitions.yml`](./validator-management.md) file or via the `POST
34+
/lighthouse/validators/web3signer` API endpoint.
35+
36+
Here is an example of a `validator_definitions.yml` file containing one validator which uses a
37+
remote signer:
38+
39+
```yaml
40+
---
41+
- enabled: true
42+
voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477"
43+
type: web3signer
44+
url: "https://my-remote-signer.com:1234"
45+
root_certificate_path: /home/paul/my-certificates/my-remote-signer.pem
46+
```
47+
48+
When using this file, the Lighthouse VC will perform duties for the `0xa5566..` validator and defer
49+
to the `https://my-remote-signer.com:1234` server to obtain any signatures. It will load a
50+
"self-signed" SSL certificate from `/home/paul/my-certificates/my-remote-signer.pem` (on the
51+
filesystem of the VC) to encrypt the communications between the VC and Web3Signer.
52+
53+
> The `request_timeout_ms` key can also be specified. Use this key to override the default timeout
54+
> with a new timeout in milliseconds. This is the timeout before requests to Web3Signer are
55+
> considered to be failures. Setting a value that it too-long may create contention and late duties
56+
> in the VC. Setting it too short will result in failed signatures and therefore missed duties.

common/account_utils/src/validator_definitions.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ pub enum SigningDefinition {
6161
#[serde(skip_serializing_if = "Option::is_none")]
6262
voting_keystore_password: Option<ZeroizeString>,
6363
},
64+
/// A validator that defers to a Web3Signer HTTP server for signing.
65+
///
66+
/// https://github.com/ConsenSys/web3signer
67+
#[serde(rename = "web3signer")]
68+
Web3Signer {
69+
url: String,
70+
/// Path to a .pem file.
71+
#[serde(skip_serializing_if = "Option::is_none")]
72+
root_certificate_path: Option<PathBuf>,
73+
/// Specifies a request timeout.
74+
///
75+
/// The timeout is applied from when the request starts connecting until the response body has finished.
76+
#[serde(skip_serializing_if = "Option::is_none")]
77+
request_timeout_ms: Option<u64>,
78+
},
6479
}
6580

6681
/// A validator that may be initialized by this validator client.
@@ -116,6 +131,12 @@ impl ValidatorDefinition {
116131
#[derive(Default, Serialize, Deserialize)]
117132
pub struct ValidatorDefinitions(Vec<ValidatorDefinition>);
118133

134+
impl From<Vec<ValidatorDefinition>> for ValidatorDefinitions {
135+
fn from(vec: Vec<ValidatorDefinition>) -> Self {
136+
Self(vec)
137+
}
138+
}
139+
119140
impl ValidatorDefinitions {
120141
/// Open an existing file or create a new, empty one if it does not exist.
121142
pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
@@ -167,11 +188,13 @@ impl ValidatorDefinitions {
167188
let known_paths: HashSet<&PathBuf> = self
168189
.0
169190
.iter()
170-
.map(|def| match &def.signing_definition {
191+
.filter_map(|def| match &def.signing_definition {
171192
SigningDefinition::LocalKeystore {
172193
voting_keystore_path,
173194
..
174-
} => voting_keystore_path,
195+
} => Some(voting_keystore_path),
196+
// A Web3Signer validator does not use a local keystore file.
197+
SigningDefinition::Web3Signer { .. } => None,
175198
})
176199
.collect();
177200

common/eth2/src/lighthouse_vc/http_client.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,22 @@ impl ValidatorClientHttpClient {
313313
self.post(path, &request).await
314314
}
315315

316+
/// `POST lighthouse/validators/web3signer`
317+
pub async fn post_lighthouse_validators_web3signer(
318+
&self,
319+
request: &[Web3SignerValidatorRequest],
320+
) -> Result<GenericResponse<ValidatorData>, Error> {
321+
let mut path = self.server.full.clone();
322+
323+
path.path_segments_mut()
324+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
325+
.push("lighthouse")
326+
.push("validators")
327+
.push("web3signer");
328+
329+
self.post(path, &request).await
330+
}
331+
316332
/// `PATCH lighthouse/validators/{validator_pubkey}`
317333
pub async fn patch_lighthouse_validators(
318334
&self,

common/eth2/src/lighthouse_vc/types.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use account_utils::ZeroizeString;
22
use eth2_keystore::Keystore;
33
use graffiti::GraffitiString;
44
use serde::{Deserialize, Serialize};
5+
use std::path::PathBuf;
56

67
pub use crate::lighthouse::Health;
78
pub use crate::types::{GenericResponse, VersionData};
@@ -64,3 +65,20 @@ pub struct KeystoreValidatorsPostRequest {
6465
pub keystore: Keystore,
6566
pub graffiti: Option<GraffitiString>,
6667
}
68+
69+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70+
pub struct Web3SignerValidatorRequest {
71+
pub enable: bool,
72+
pub description: String,
73+
#[serde(default)]
74+
#[serde(skip_serializing_if = "Option::is_none")]
75+
pub graffiti: Option<GraffitiString>,
76+
pub voting_public_key: PublicKey,
77+
pub url: String,
78+
#[serde(default)]
79+
#[serde(skip_serializing_if = "Option::is_none")]
80+
pub root_certificate_path: Option<PathBuf>,
81+
#[serde(default)]
82+
#[serde(skip_serializing_if = "Option::is_none")]
83+
pub request_timeout_ms: Option<u64>,
84+
}

common/lighthouse_metrics/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,18 @@ pub fn set_gauge_vec(int_gauge_vec: &Result<IntGaugeVec>, name: &[&str], value:
283283
}
284284
}
285285

286+
pub fn inc_gauge_vec(int_gauge_vec: &Result<IntGaugeVec>, name: &[&str]) {
287+
if let Some(gauge) = get_int_gauge(int_gauge_vec, name) {
288+
gauge.inc();
289+
}
290+
}
291+
292+
pub fn dec_gauge_vec(int_gauge_vec: &Result<IntGaugeVec>, name: &[&str]) {
293+
if let Some(gauge) = get_int_gauge(int_gauge_vec, name) {
294+
gauge.dec();
295+
}
296+
}
297+
286298
pub fn set_gauge(gauge: &Result<IntGauge>, value: i64) {
287299
if let Ok(gauge) = gauge {
288300
gauge.set(value);

common/task_executor/src/lib.rs

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -237,39 +237,33 @@ impl TaskExecutor {
237237
{
238238
let log = self.log.clone();
239239

240-
if let Some(metric) = metrics::get_histogram(&metrics::BLOCKING_TASKS_HISTOGRAM, &[name]) {
241-
if let Some(int_gauge) = metrics::get_int_gauge(&metrics::BLOCKING_TASKS_COUNT, &[name])
242-
{
243-
let int_gauge_1 = int_gauge;
244-
let timer = metric.start_timer();
245-
let join_handle = if let Some(runtime) = self.runtime.upgrade() {
246-
runtime.spawn_blocking(task)
247-
} else {
248-
debug!(self.log, "Couldn't spawn task. Runtime shutting down");
249-
return None;
250-
};
240+
let timer = metrics::start_timer_vec(&metrics::BLOCKING_TASKS_HISTOGRAM, &[name]);
241+
metrics::inc_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]);
251242

252-
Some(async move {
253-
let result = match join_handle.await {
254-
Ok(result) => {
255-
trace!(log, "Blocking task completed"; "task" => name);
256-
Ok(result)
257-
}
258-
Err(e) => {
259-
debug!(log, "Blocking task ended unexpectedly"; "error" => %e);
260-
Err(e)
261-
}
262-
};
263-
timer.observe_duration();
264-
int_gauge_1.dec();
265-
result
266-
})
267-
} else {
268-
None
269-
}
243+
let join_handle = if let Some(runtime) = self.runtime.upgrade() {
244+
runtime.spawn_blocking(task)
270245
} else {
271-
None
272-
}
246+
debug!(self.log, "Couldn't spawn task. Runtime shutting down");
247+
return None;
248+
};
249+
250+
let future = async move {
251+
let result = match join_handle.await {
252+
Ok(result) => {
253+
trace!(log, "Blocking task completed"; "task" => name);
254+
Ok(result)
255+
}
256+
Err(e) => {
257+
debug!(log, "Blocking task ended unexpectedly"; "error" => %e);
258+
Err(e)
259+
}
260+
};
261+
drop(timer);
262+
metrics::dec_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]);
263+
result
264+
};
265+
266+
Some(future)
273267
}
274268

275269
pub fn runtime(&self) -> Weak<Runtime> {

0 commit comments

Comments
 (0)