Skip to content

Commit 3d276f8

Browse files
jimmygchenWoodpile37
authored andcommitted
Add support for updating validator graffiti (sigp#4417)
## Issue Addressed sigp#4386 ## Proposed Changes The original proposal described in the issue adds a new endpoint to support updating validator graffiti, but I realized we already have an endpoint that we use for updating various validator fields in memory and in the validator definitions file, so I think that would be the best place to add this to. ### API endpoint `PATCH lighthouse/validators/{validator_pubkey}` This endpoint updates the graffiti in both the [ validator definition file](https://lighthouse-book.sigmaprime.io/graffiti.html#2-setting-the-graffiti-in-the-validator_definitionsyml) and the in memory `InitializedValidators`. In the next block proposal, the new graffiti will be used. Note that the [`--graffiti-file`](https://lighthouse-book.sigmaprime.io/graffiti.html#1-using-the---graffiti-file-flag-on-the-validator-client) flag has a priority over the validator definitions file, so if the caller attempts to update the graffiti while the `--graffiti-file` flag is present, the endpoint will return an error (Bad request 400). ## Tasks - [x] Add graffiti update support to `PATCH lighthouse/validators/{validator_pubkey}` - [x] Return error if user tries to update graffiti while the `--graffiti-flag` is set - [x] Update Lighthouse book
1 parent 2a3eea8 commit 3d276f8

File tree

8 files changed

+143
-11
lines changed

8 files changed

+143
-11
lines changed

book/src/api-vc-endpoints.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,8 @@ Example Response Body
426426

427427
## `PATCH /lighthouse/validators/:voting_pubkey`
428428

429-
Update some values for the validator with `voting_pubkey`. The following example updates a validator from `enabled: true` to `enabled: false`
429+
Update some values for the validator with `voting_pubkey`. Possible fields: `enabled`, `gas_limit`, `builder_proposals`,
430+
and `graffiti`. The following example updates a validator from `enabled: true` to `enabled: false`.
430431

431432
### HTTP Specification
432433

book/src/graffiti.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Lighthouse will first search for the graffiti corresponding to the public key of
2929
### 2. Setting the graffiti in the `validator_definitions.yml`
3030
Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal.
3131

32+
You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http).
33+
3234
Below is an example of the validator_definitions.yml with validator specific graffitis:
3335
```
3436
---
@@ -62,3 +64,25 @@ Usage: `lighthouse bn --graffiti fortytwo`
6264
> 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client.
6365
> 4. If the `--graffiti` flag on the validator client is not passed, load the graffiti passed in the `--graffiti` flag on the beacon node.
6466
> 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti.
67+
68+
### Set Graffiti via HTTP
69+
70+
Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti
71+
both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal
72+
without requiring a validator client restart.
73+
74+
Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification.
75+
76+
#### Example Command
77+
78+
```bash
79+
DATADIR=/var/lib/lighthouse
80+
curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \
81+
-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \
82+
-H "Content-Type: application/json" \
83+
-d '{
84+
"graffiti": "Mr F was here"
85+
}' | jq
86+
```
87+
88+
A `null` response indicates that the request is successful.

common/eth2/src/lighthouse_vc/http_client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::path::Path;
1616

1717
pub use reqwest;
1818
pub use reqwest::{Response, StatusCode, Url};
19+
use types::graffiti::GraffitiString;
1920

2021
/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a
2122
/// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`).
@@ -467,6 +468,7 @@ impl ValidatorClientHttpClient {
467468
enabled: Option<bool>,
468469
gas_limit: Option<u64>,
469470
builder_proposals: Option<bool>,
471+
graffiti: Option<GraffitiString>,
470472
) -> Result<(), Error> {
471473
let mut path = self.server.full.clone();
472474

@@ -482,6 +484,7 @@ impl ValidatorClientHttpClient {
482484
enabled,
483485
gas_limit,
484486
builder_proposals,
487+
graffiti,
485488
},
486489
)
487490
.await

common/eth2/src/lighthouse_vc/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ pub struct ValidatorPatchRequest {
8383
#[serde(default)]
8484
#[serde(skip_serializing_if = "Option::is_none")]
8585
pub builder_proposals: Option<bool>,
86+
#[serde(default)]
87+
#[serde(skip_serializing_if = "Option::is_none")]
88+
pub graffiti: Option<GraffitiString>,
8689
}
8790

8891
#[derive(Clone, PartialEq, Serialize, Deserialize)]

validator_client/src/http_api/mod.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
357357
.and(warp::path("graffiti"))
358358
.and(warp::path::end())
359359
.and(validator_store_filter.clone())
360-
.and(graffiti_file_filter)
360+
.and(graffiti_file_filter.clone())
361361
.and(graffiti_flag_filter)
362362
.and(signer.clone())
363363
.and(log_filter.clone())
@@ -617,18 +617,27 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
617617
.and(warp::path::end())
618618
.and(warp::body::json())
619619
.and(validator_store_filter.clone())
620+
.and(graffiti_file_filter)
620621
.and(signer.clone())
621622
.and(task_executor_filter.clone())
622623
.and_then(
623624
|validator_pubkey: PublicKey,
624625
body: api_types::ValidatorPatchRequest,
625626
validator_store: Arc<ValidatorStore<T, E>>,
627+
graffiti_file: Option<GraffitiFile>,
626628
signer,
627629
task_executor: TaskExecutor| {
628630
blocking_signed_json_task(signer, move || {
631+
if body.graffiti.is_some() && graffiti_file.is_some() {
632+
return Err(warp_utils::reject::custom_bad_request(
633+
"Unable to update graffiti as the \"--graffiti-file\" flag is set"
634+
.to_string(),
635+
));
636+
}
637+
638+
let maybe_graffiti = body.graffiti.clone().map(Into::into);
629639
let initialized_validators_rw_lock = validator_store.initialized_validators();
630640
let mut initialized_validators = initialized_validators_rw_lock.write();
631-
632641
match (
633642
initialized_validators.is_enabled(&validator_pubkey),
634643
initialized_validators.validator(&validator_pubkey.compress()),
@@ -641,7 +650,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
641650
if Some(is_enabled) == body.enabled
642651
&& initialized_validator.get_gas_limit() == body.gas_limit
643652
&& initialized_validator.get_builder_proposals()
644-
== body.builder_proposals =>
653+
== body.builder_proposals
654+
&& initialized_validator.get_graffiti() == maybe_graffiti =>
645655
{
646656
Ok(())
647657
}
@@ -654,6 +664,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
654664
body.enabled,
655665
body.gas_limit,
656666
body.builder_proposals,
667+
body.graffiti,
657668
),
658669
)
659670
.map_err(|e| {

validator_client/src/http_api/tests.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ use slot_clock::{SlotClock, TestingSlotClock};
2828
use std::future::Future;
2929
use std::marker::PhantomData;
3030
use std::net::{IpAddr, Ipv4Addr};
31+
use std::str::FromStr;
3132
use std::sync::Arc;
3233
use std::time::Duration;
3334
use task_executor::TaskExecutor;
3435
use tempfile::{tempdir, TempDir};
3536
use tokio::runtime::Runtime;
3637
use tokio::sync::oneshot;
38+
use types::graffiti::GraffitiString;
3739

3840
const PASSWORD_BYTES: &[u8] = &[42, 50, 37];
3941
pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42);
@@ -533,7 +535,7 @@ impl ApiTester {
533535
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
534536

535537
self.client
536-
.patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None)
538+
.patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None)
537539
.await
538540
.unwrap();
539541

@@ -575,7 +577,13 @@ impl ApiTester {
575577
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
576578

577579
self.client
578-
.patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None)
580+
.patch_lighthouse_validators(
581+
&validator.voting_pubkey,
582+
None,
583+
Some(gas_limit),
584+
None,
585+
None,
586+
)
579587
.await
580588
.unwrap();
581589

@@ -602,6 +610,7 @@ impl ApiTester {
602610
None,
603611
None,
604612
Some(builder_proposals),
613+
None,
605614
)
606615
.await
607616
.unwrap();
@@ -620,6 +629,34 @@ impl ApiTester {
620629

621630
self
622631
}
632+
633+
pub async fn set_graffiti(self, index: usize, graffiti: &str) -> Self {
634+
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
635+
let graffiti_str = GraffitiString::from_str(graffiti).unwrap();
636+
self.client
637+
.patch_lighthouse_validators(
638+
&validator.voting_pubkey,
639+
None,
640+
None,
641+
None,
642+
Some(graffiti_str),
643+
)
644+
.await
645+
.unwrap();
646+
647+
self
648+
}
649+
650+
pub async fn assert_graffiti(self, index: usize, graffiti: &str) -> Self {
651+
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
652+
let graffiti_str = GraffitiString::from_str(graffiti).unwrap();
653+
assert_eq!(
654+
self.validator_store.graffiti(&validator.voting_pubkey),
655+
Some(graffiti_str.into())
656+
);
657+
658+
self
659+
}
623660
}
624661

625662
struct HdValidatorScenario {
@@ -723,7 +760,13 @@ fn routes_with_invalid_auth() {
723760
.await
724761
.test_with_invalid_auth(|client| async move {
725762
client
726-
.patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None)
763+
.patch_lighthouse_validators(
764+
&PublicKeyBytes::empty(),
765+
Some(false),
766+
None,
767+
None,
768+
None,
769+
)
727770
.await
728771
})
729772
.await
@@ -931,6 +974,41 @@ fn validator_builder_proposals() {
931974
});
932975
}
933976

977+
#[test]
978+
fn validator_graffiti() {
979+
let runtime = build_runtime();
980+
let weak_runtime = Arc::downgrade(&runtime);
981+
runtime.block_on(async {
982+
ApiTester::new(weak_runtime)
983+
.await
984+
.create_hd_validators(HdValidatorScenario {
985+
count: 2,
986+
specify_mnemonic: false,
987+
key_derivation_path_offset: 0,
988+
disabled: vec![],
989+
})
990+
.await
991+
.assert_enabled_validators_count(2)
992+
.assert_validators_count(2)
993+
.set_graffiti(0, "Mr F was here")
994+
.await
995+
.assert_graffiti(0, "Mr F was here")
996+
.await
997+
// Test setting graffiti while the validator is disabled
998+
.set_validator_enabled(0, false)
999+
.await
1000+
.assert_enabled_validators_count(1)
1001+
.assert_validators_count(2)
1002+
.set_graffiti(0, "Mr F was here again")
1003+
.await
1004+
.set_validator_enabled(0, true)
1005+
.await
1006+
.assert_enabled_validators_count(2)
1007+
.assert_graffiti(0, "Mr F was here again")
1008+
.await
1009+
});
1010+
}
1011+
9341012
#[test]
9351013
fn keystore_validator_creation() {
9361014
let runtime = build_runtime();

validator_client/src/http_api/tests/keystores.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() {
468468
for pubkey in &pubkeys {
469469
tester
470470
.client
471-
.patch_lighthouse_validators(pubkey, Some(false), None, None)
471+
.patch_lighthouse_validators(pubkey, Some(false), None, None, None)
472472
.await
473473
.unwrap();
474474
}

validator_client/src/initialized_validators.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use std::io::{self, Read};
2727
use std::path::{Path, PathBuf};
2828
use std::sync::Arc;
2929
use std::time::Duration;
30+
use types::graffiti::GraffitiString;
3031
use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes};
3132
use url::{ParseError, Url};
3233
use validator_dir::Builder as ValidatorDirBuilder;
@@ -147,6 +148,10 @@ impl InitializedValidator {
147148
pub fn get_index(&self) -> Option<u64> {
148149
self.index
149150
}
151+
152+
pub fn get_graffiti(&self) -> Option<Graffiti> {
153+
self.graffiti
154+
}
150155
}
151156

152157
fn open_keystore(path: &Path) -> Result<Keystore, Error> {
@@ -671,8 +676,8 @@ impl InitializedValidators {
671676
self.validators.get(public_key)
672677
}
673678

674-
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals`
675-
/// values.
679+
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`,
680+
/// `builder_proposals`, and `graffiti` values.
676681
///
677682
/// ## Notes
678683
///
@@ -682,7 +687,7 @@ impl InitializedValidators {
682687
///
683688
/// If a `gas_limit` is included in the call to this function, it will also be updated and saved
684689
/// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition`
685-
/// or `InitializedValidator`. The same logic applies to `builder_proposals`.
690+
/// or `InitializedValidator`. The same logic applies to `builder_proposals` and `graffiti`.
686691
///
687692
/// Saves the `ValidatorDefinitions` to file, even if no definitions were changed.
688693
pub async fn set_validator_definition_fields(
@@ -691,6 +696,7 @@ impl InitializedValidators {
691696
enabled: Option<bool>,
692697
gas_limit: Option<u64>,
693698
builder_proposals: Option<bool>,
699+
graffiti: Option<GraffitiString>,
694700
) -> Result<(), Error> {
695701
if let Some(def) = self
696702
.definitions
@@ -708,6 +714,9 @@ impl InitializedValidators {
708714
if let Some(builder_proposals) = builder_proposals {
709715
def.builder_proposals = Some(builder_proposals);
710716
}
717+
if let Some(graffiti) = graffiti.clone() {
718+
def.graffiti = Some(graffiti);
719+
}
711720
}
712721

713722
self.update_validators().await?;
@@ -723,6 +732,9 @@ impl InitializedValidators {
723732
if let Some(builder_proposals) = builder_proposals {
724733
val.builder_proposals = Some(builder_proposals);
725734
}
735+
if let Some(graffiti) = graffiti {
736+
val.graffiti = Some(graffiti.into());
737+
}
726738
}
727739

728740
self.definitions

0 commit comments

Comments
 (0)