Skip to content

Commit 8497de8

Browse files
AgeManningmichaelsproulpaulhauner
authored andcommitted
Separate BN for block proposals (sigp#4182)
It is a well-known fact that IP addresses for beacon nodes used by specific validators can be de-anonymized. There is an assumed risk that a malicious user may attempt to DOS validators when producing blocks to prevent chain growth/liveness. Although there are a number of ideas put forward to address this, there a few simple approaches we can take to mitigate this risk. Currently, a Lighthouse user is able to set a number of beacon-nodes that their validator client can connect to. If one beacon node is taken offline, it can fallback to another. Different beacon nodes can use VPNs or rotate IPs in order to mask their IPs. This PR provides an additional setup option which further mitigates attacks of this kind. This PR introduces a CLI flag --proposer-only to the beacon node. Setting this flag will configure the beacon node to run with minimal peers and crucially will not subscribe to subnets or sync committees. Therefore nodes of this kind should not be identified as nodes connected to validators of any kind. It also introduces a CLI flag --proposer-nodes to the validator client. Users can then provide a number of beacon nodes (which may or may not run the --proposer-only flag) that the Validator client will use for block production and propagation only. If these nodes fail, the validator client will fallback to the default list of beacon nodes. Users are then able to set up a number of beacon nodes dedicated to block proposals (which are unlikely to be identified as validator nodes) and point their validator clients to produce blocks on these nodes and attest on other beacon nodes. An attack attempting to prevent liveness on the eth2 network would then need to preemptively find and attack the proposer nodes which is significantly more difficult than the default setup. This is a follow on from: sigp#3328 Co-authored-by: Michael Sproul <[email protected]> Co-authored-by: Paul Hauner <[email protected]>
1 parent 3eb763b commit 8497de8

File tree

16 files changed

+452
-75
lines changed

16 files changed

+452
-75
lines changed

beacon_node/lighthouse_network/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ pub struct Config {
134134
/// List of extra topics to initially subscribe to as strings.
135135
pub topics: Vec<GossipKind>,
136136

137+
/// Whether we are running a block proposer only node.
138+
pub proposer_only: bool,
139+
137140
/// Whether metrics are enabled.
138141
pub metrics_enabled: bool,
139142

@@ -322,6 +325,7 @@ impl Default for Config {
322325
import_all_attestations: false,
323326
shutdown_after_sync: false,
324327
topics: Vec::new(),
328+
proposer_only: false,
325329
metrics_enabled: false,
326330
enable_light_client_server: false,
327331
outbound_rate_limiter_config: None,

beacon_node/network/src/subnet_service/attestation_subnets.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ pub struct AttestationService<T: BeaconChainTypes> {
112112
#[cfg(feature = "deterministic_long_lived_attnets")]
113113
next_long_lived_subscription_event: Pin<Box<tokio::time::Sleep>>,
114114

115+
/// Whether this node is a block proposer-only node.
116+
proposer_only: bool,
117+
115118
/// The logger for the attestation service.
116119
log: slog::Logger,
117120
}
@@ -155,6 +158,7 @@ impl<T: BeaconChainTypes> AttestationService<T> {
155158
known_validators: HashSetDelay::new(last_seen_val_timeout),
156159
waker: None,
157160
discovery_disabled: config.disable_discovery,
161+
proposer_only: config.proposer_only,
158162
subscribe_all_subnets: config.subscribe_all_subnets,
159163
long_lived_subnet_subscription_slots,
160164
log,
@@ -256,6 +260,11 @@ impl<T: BeaconChainTypes> AttestationService<T> {
256260
&mut self,
257261
subscriptions: Vec<ValidatorSubscription>,
258262
) -> Result<(), String> {
263+
// If the node is in a proposer-only state, we ignore all subnet subscriptions.
264+
if self.proposer_only {
265+
return Ok(());
266+
}
267+
259268
// Maps each subnet_id subscription to it's highest slot
260269
let mut subnets_to_discover: HashMap<SubnetId, Slot> = HashMap::new();
261270
for subscription in subscriptions {
@@ -450,6 +459,10 @@ impl<T: BeaconChainTypes> AttestationService<T> {
450459
subnet: SubnetId,
451460
attestation: &Attestation<T::EthSpec>,
452461
) -> bool {
462+
// Proposer-only mode does not need to process attestations
463+
if self.proposer_only {
464+
return false;
465+
}
453466
self.aggregate_validators_on_subnet
454467
.as_ref()
455468
.map(|tracked_vals| {

beacon_node/network/src/subnet_service/sync_subnets.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub struct SyncCommitteeService<T: BeaconChainTypes> {
5454
/// We are always subscribed to all subnets.
5555
subscribe_all_subnets: bool,
5656

57+
/// Whether this node is a block proposer-only node.
58+
proposer_only: bool,
59+
5760
/// The logger for the attestation service.
5861
log: slog::Logger,
5962
}
@@ -82,6 +85,7 @@ impl<T: BeaconChainTypes> SyncCommitteeService<T> {
8285
waker: None,
8386
subscribe_all_subnets: config.subscribe_all_subnets,
8487
discovery_disabled: config.disable_discovery,
88+
proposer_only: config.proposer_only,
8589
log,
8690
}
8791
}
@@ -110,6 +114,11 @@ impl<T: BeaconChainTypes> SyncCommitteeService<T> {
110114
&mut self,
111115
subscriptions: Vec<SyncCommitteeSubscription>,
112116
) -> Result<(), String> {
117+
// A proposer-only node does not subscribe to any sync-committees
118+
if self.proposer_only {
119+
return Ok(());
120+
}
121+
113122
let mut subnets_to_discover = Vec::new();
114123
for subscription in subscriptions {
115124
metrics::inc_counter(&metrics::SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS);

beacon_node/src/cli.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
123123
Arg::with_name("target-peers")
124124
.long("target-peers")
125125
.help("The target number of peers.")
126-
.default_value("80")
127126
.takes_value(true),
128127
)
129128
.arg(
@@ -269,6 +268,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
269268
.min_values(0)
270269
.hidden(true)
271270
)
271+
.arg(
272+
Arg::with_name("proposer-only")
273+
.long("proposer-only")
274+
.help("Sets this beacon node at be a block proposer only node. \
275+
This will run the beacon node in a minimal configuration that is sufficient for block publishing only. This flag should be used \
276+
for a beacon node being referenced by validator client using the --proposer-node flag. This configuration is for enabling more secure setups.")
277+
.takes_value(false),
278+
)
279+
272280
.arg(
273281
Arg::with_name("disable-backfill-rate-limiting")
274282
.long("disable-backfill-rate-limiting")

beacon_node/src/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,10 +979,13 @@ pub fn set_network_config(
979979

980980
config.set_listening_addr(parse_listening_addresses(cli_args, log)?);
981981

982+
// A custom target-peers command will overwrite the --proposer-only default.
982983
if let Some(target_peers_str) = cli_args.value_of("target-peers") {
983984
config.target_peers = target_peers_str
984985
.parse::<usize>()
985986
.map_err(|_| format!("Invalid number of target peers: {}", target_peers_str))?;
987+
} else {
988+
config.target_peers = 80; // default value
986989
}
987990

988991
if let Some(value) = cli_args.value_of("network-load") {
@@ -1218,6 +1221,20 @@ pub fn set_network_config(
12181221
config.outbound_rate_limiter_config = Some(Default::default());
12191222
}
12201223

1224+
// Proposer-only mode overrides a number of previous configuration parameters.
1225+
// Specifically, we avoid subscribing to long-lived subnets and wish to maintain a minimal set
1226+
// of peers.
1227+
if cli_args.is_present("proposer-only") {
1228+
config.subscribe_all_subnets = false;
1229+
1230+
if cli_args.value_of("target-peers").is_none() {
1231+
// If a custom value is not set, change the default to 15
1232+
config.target_peers = 15;
1233+
}
1234+
config.proposer_only = true;
1235+
warn!(log, "Proposer-only mode enabled"; "info"=> "Do not connect a validator client to this node unless via the --proposer-nodes flag");
1236+
}
1237+
12211238
Ok(())
12221239
}
12231240

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* [Checkpoint Sync](./checkpoint-sync.md)
4242
* [Custom Data Directories](./advanced-datadir.md)
4343
* [Validator Graffiti](./graffiti.md)
44+
* [Proposer Only Beacon Nodes](./advanced-proposer-only.md)
4445
* [Remote Signing with Web3Signer](./validator-web3signer.md)
4546
* [Database Configuration](./advanced_database.md)
4647
* [Database Migrations](./database-migrations.md)

book/src/advanced-proposer-only.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Advanced Proposer-Only Beacon Nodes
2+
3+
Lighthouse allows for more exotic setups that can minimize attack vectors by
4+
adding redundant beacon nodes and dividing the roles of attesting and block
5+
production between them.
6+
7+
The purpose of this is to minimize attack vectors
8+
where malicious users obtain the network identities (IP addresses) of beacon
9+
nodes corresponding to individual validators and subsequently perform Denial Of Service
10+
attacks on the beacon nodes when they are due to produce a block on the
11+
network. By splitting the duties of attestation and block production across
12+
different beacon nodes, an attacker may not know which node is the block
13+
production node, especially if the user rotates IP addresses of the block
14+
production beacon node in between block proposals (this is in-frequent with
15+
networks with large validator counts).
16+
17+
## The Beacon Node
18+
19+
A Lighthouse beacon node can be configured with the `--proposer-only` flag
20+
(i.e. `lighthouse bn --proposer-only`).
21+
Setting a beacon node with this flag will limit its use as a beacon node for
22+
normal activities such as performing attestations, but it will make the node
23+
harder to identify as a potential node to attack and will also consume less
24+
resources.
25+
26+
Specifically, this flag reduces the default peer count (to a safe minimal
27+
number as maintaining peers on attestation subnets do not need to be considered),
28+
prevents the node from subscribing to any attestation-subnets or
29+
sync-committees which is a primary way for attackers to de-anonymize
30+
validators.
31+
32+
> Note: Beacon nodes that have set the `--proposer-only` flag should not be connected
33+
> to validator clients unless via the `--proposer-nodes` flag. If connected as a
34+
> normal beacon node, the validator may fail to handle its duties correctly and
35+
> result in a loss of income.
36+
37+
38+
## The Validator Client
39+
40+
The validator client can be given a list of HTTP API endpoints representing
41+
beacon nodes that will be solely used for block propagation on the network, via
42+
the CLI flag `--proposer-nodes`. These nodes can be any working beacon nodes
43+
and do not specifically have to be proposer-only beacon nodes that have been
44+
executed with the `--proposer-only` (although we do recommend this flag for
45+
these nodes for added security).
46+
47+
> Note: The validator client still requires at least one other beacon node to
48+
> perform its duties and must be specified in the usual `--beacon-nodes` flag.
49+
50+
> Note: The validator client will attempt to get a block to propose from the
51+
> beacon nodes specified in `--beacon-nodes` before trying `--proposer-nodes`.
52+
> This is because the nodes subscribed to subnets have a higher chance of
53+
> producing a more profitable block. Any block builders should therefore be
54+
> attached to the `--beacon-nodes` and not necessarily the `--proposer-nodes`.
55+
56+
57+
## Setup Overview
58+
59+
The intended set-up to take advantage of this mechanism is to run one (or more)
60+
normal beacon nodes in conjunction with one (or more) proposer-only beacon
61+
nodes. See the [Redundancy](./redundancy.md) section for more information about
62+
setting up redundant beacon nodes. The proposer-only beacon nodes should be
63+
setup to use a different IP address than the primary (non proposer-only) nodes.
64+
For added security, the IP addresses of the proposer-only nodes should be
65+
rotated occasionally such that a new IP-address is used per block proposal.
66+
67+
A single validator client can then connect to all of the above nodes via the
68+
`--beacon-nodes` and `--proposer-nodes` flags. The resulting setup will allow
69+
the validator client to perform its regular duties on the standard beacon nodes
70+
and when the time comes to propose a block, it will send this block via the
71+
specified proposer-only nodes.

testing/simulator/src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
2424
.takes_value(true)
2525
.default_value("4")
2626
.help("Number of beacon nodes"))
27+
.arg(Arg::with_name("proposer-nodes")
28+
.short("n")
29+
.long("nodes")
30+
.takes_value(true)
31+
.default_value("2")
32+
.help("Number of proposer-only beacon nodes"))
2733
.arg(Arg::with_name("validators_per_node")
2834
.short("v")
2935
.long("validators_per_node")
@@ -57,6 +63,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
5763
.takes_value(true)
5864
.default_value("4")
5965
.help("Number of beacon nodes"))
66+
.arg(Arg::with_name("proposer-nodes")
67+
.short("n")
68+
.long("nodes")
69+
.takes_value(true)
70+
.default_value("2")
71+
.help("Number of proposer-only beacon nodes"))
6072
.arg(Arg::with_name("validators_per_node")
6173
.short("v")
6274
.long("validators_per_node")

testing/simulator/src/eth1_sim.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const SUGGESTED_FEE_RECIPIENT: [u8; 20] =
2727

2828
pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
2929
let node_count = value_t!(matches, "nodes", usize).expect("missing nodes default");
30+
let proposer_nodes = value_t!(matches, "proposer-nodes", usize).unwrap_or(0);
31+
println!("PROPOSER-NODES: {}", proposer_nodes);
3032
let validators_per_node = value_t!(matches, "validators_per_node", usize)
3133
.expect("missing validators_per_node default");
3234
let speed_up_factor =
@@ -35,7 +37,8 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
3537
let post_merge_sim = matches.is_present("post-merge");
3638

3739
println!("Beacon Chain Simulator:");
38-
println!(" nodes:{}", node_count);
40+
println!(" nodes:{}, proposer_nodes: {}", node_count, proposer_nodes);
41+
3942
println!(" validators_per_node:{}", validators_per_node);
4043
println!(" post merge simulation:{}", post_merge_sim);
4144
println!(" continue_after_checks:{}", continue_after_checks);
@@ -147,7 +150,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
147150
beacon_config.sync_eth1_chain = true;
148151
beacon_config.eth1.auto_update_interval_millis = eth1_block_time.as_millis() as u64;
149152
beacon_config.eth1.chain_id = Eth1Id::from(chain_id);
150-
beacon_config.network.target_peers = node_count - 1;
153+
beacon_config.network.target_peers = node_count + proposer_nodes - 1;
151154

152155
beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None);
153156

@@ -173,7 +176,17 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
173176
* One by one, add beacon nodes to the network.
174177
*/
175178
for _ in 0..node_count - 1 {
176-
network.add_beacon_node(beacon_config.clone()).await?;
179+
network
180+
.add_beacon_node(beacon_config.clone(), false)
181+
.await?;
182+
}
183+
184+
/*
185+
* One by one, add proposer nodes to the network.
186+
*/
187+
for _ in 0..proposer_nodes - 1 {
188+
println!("Adding a proposer node");
189+
network.add_beacon_node(beacon_config.clone(), true).await?;
177190
}
178191

179192
/*
@@ -310,7 +323,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
310323
*/
311324
println!(
312325
"Simulation complete. Finished with {} beacon nodes and {} validator clients",
313-
network.beacon_node_count(),
326+
network.beacon_node_count() + network.proposer_node_count(),
314327
network.validator_client_count()
315328
);
316329

0 commit comments

Comments
 (0)