Skip to content

Commit 781c5ec

Browse files
Add lcli command for manual rescue sync (#5458)
* Rescue CLI * Allow tweaking start block * More caching * Merge branch 'unstable' into rescue-cli # Conflicts: # lcli/src/main.rs * Add `--known–common-ancestor` flag to optimise for download speed. * Rename rescue command to `http-sync` * Add logging * Add optional `--block-cache-dir` cli arg and create directory if it doesn't already exist. * Lint fix. * Merge branch 'unstable' into rescue-cli
1 parent 3913ea4 commit 781c5ec

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed

lcli/src/http_sync.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use clap::ArgMatches;
2+
use clap_utils::{parse_optional, parse_required};
3+
use environment::Environment;
4+
use eth2::{
5+
types::{BlockId, ChainSpec, ForkName, PublishBlockRequest, SignedBlockContents},
6+
BeaconNodeHttpClient, Error, SensitiveUrl, Timeouts,
7+
};
8+
use eth2_network_config::Eth2NetworkConfig;
9+
use ssz::Encode;
10+
use std::fs;
11+
use std::fs::File;
12+
use std::io::{Read, Write};
13+
use std::path::{Path, PathBuf};
14+
use std::sync::Arc;
15+
use std::time::Duration;
16+
use types::EthSpec;
17+
18+
const HTTP_TIMEOUT: Duration = Duration::from_secs(3600);
19+
const DEFAULT_CACHE_DIR: &str = "./cache";
20+
21+
pub fn run<T: EthSpec>(
22+
env: Environment<T>,
23+
network_config: Eth2NetworkConfig,
24+
matches: &ArgMatches,
25+
) -> Result<(), String> {
26+
let executor = env.core_context().executor;
27+
executor
28+
.handle()
29+
.ok_or("shutdown in progress")?
30+
.block_on(async move { run_async::<T>(network_config, matches).await })
31+
}
32+
33+
pub async fn run_async<T: EthSpec>(
34+
network_config: Eth2NetworkConfig,
35+
matches: &ArgMatches,
36+
) -> Result<(), String> {
37+
let spec = &network_config.chain_spec::<T>()?;
38+
let source_url: SensitiveUrl = parse_required(matches, "source-url")?;
39+
let target_url: SensitiveUrl = parse_required(matches, "target-url")?;
40+
let start_block: BlockId = parse_required(matches, "start-block")?;
41+
let maybe_common_ancestor_block: Option<BlockId> =
42+
parse_optional(matches, "known–common-ancestor")?;
43+
let cache_dir_path: PathBuf =
44+
parse_optional(matches, "block-cache-dir")?.unwrap_or(DEFAULT_CACHE_DIR.into());
45+
46+
let source = BeaconNodeHttpClient::new(source_url, Timeouts::set_all(HTTP_TIMEOUT));
47+
let target = BeaconNodeHttpClient::new(target_url, Timeouts::set_all(HTTP_TIMEOUT));
48+
49+
if !cache_dir_path.exists() {
50+
fs::create_dir_all(&cache_dir_path)
51+
.map_err(|e| format!("Unable to create block cache dir: {:?}", e))?;
52+
}
53+
54+
// 1. Download blocks back from head, looking for common ancestor.
55+
let mut blocks = vec![];
56+
let mut next_block_id = start_block;
57+
loop {
58+
println!("downloading {next_block_id:?}");
59+
60+
let publish_block_req =
61+
get_block_from_source::<T>(&source, next_block_id, spec, &cache_dir_path).await;
62+
let block = publish_block_req.signed_block();
63+
64+
next_block_id = BlockId::Root(block.parent_root());
65+
blocks.push((block.slot(), publish_block_req));
66+
67+
if let Some(ref common_ancestor_block) = maybe_common_ancestor_block {
68+
if common_ancestor_block == &next_block_id {
69+
println!("reached known common ancestor: {next_block_id:?}");
70+
break;
71+
}
72+
}
73+
74+
let block_exists_in_target = target
75+
.get_beacon_blocks_ssz::<T>(next_block_id, spec)
76+
.await
77+
.unwrap()
78+
.is_some();
79+
if block_exists_in_target {
80+
println!("common ancestor found: {next_block_id:?}");
81+
break;
82+
}
83+
}
84+
85+
// 2. Apply blocks to target.
86+
for (slot, block) in blocks.iter().rev() {
87+
println!("posting block at slot {slot}");
88+
if let Err(e) = target.post_beacon_blocks(block).await {
89+
if let Error::ServerMessage(ref e) = e {
90+
if e.code == 202 {
91+
println!("duplicate block detected while posting block at slot {slot}");
92+
continue;
93+
}
94+
}
95+
return Err(format!("error posting {slot}: {e:?}"));
96+
} else {
97+
println!("success");
98+
}
99+
}
100+
101+
println!("SYNCED!!!!");
102+
103+
Ok(())
104+
}
105+
106+
async fn get_block_from_source<T: EthSpec>(
107+
source: &BeaconNodeHttpClient,
108+
block_id: BlockId,
109+
spec: &ChainSpec,
110+
cache_dir_path: &Path,
111+
) -> PublishBlockRequest<T> {
112+
let mut cache_path = cache_dir_path.join(format!("block_{block_id}"));
113+
114+
if cache_path.exists() {
115+
let mut f = File::open(&cache_path).unwrap();
116+
let mut bytes = vec![];
117+
f.read_to_end(&mut bytes).unwrap();
118+
PublishBlockRequest::from_ssz_bytes(&bytes, ForkName::Deneb).unwrap()
119+
} else {
120+
let block_from_source = source
121+
.get_beacon_blocks_ssz::<T>(block_id, spec)
122+
.await
123+
.unwrap()
124+
.unwrap();
125+
let blobs_from_source = source
126+
.get_blobs::<T>(block_id, None)
127+
.await
128+
.unwrap()
129+
.unwrap()
130+
.data;
131+
132+
let (kzg_proofs, blobs): (Vec<_>, Vec<_>) = blobs_from_source
133+
.iter()
134+
.cloned()
135+
.map(|sidecar| (sidecar.kzg_proof, sidecar.blob.clone()))
136+
.unzip();
137+
138+
let block_root = block_from_source.canonical_root();
139+
let block_contents = SignedBlockContents {
140+
signed_block: Arc::new(block_from_source),
141+
kzg_proofs: kzg_proofs.into(),
142+
blobs: blobs.into(),
143+
};
144+
let publish_block_req = PublishBlockRequest::BlockContents(block_contents);
145+
146+
cache_path = cache_dir_path.join(format!("block_{block_root:?}"));
147+
let mut f = File::create(&cache_path).unwrap();
148+
f.write_all(&publish_block_req.as_ssz_bytes()).unwrap();
149+
150+
publish_block_req
151+
}
152+
}

lcli/src/main.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod block_root;
22
mod check_deposit_data;
33
mod generate_bootnode_enr;
4+
mod http_sync;
45
mod indexed_attestations;
56
mod mnemonic_validators;
67
mod mock_el;
@@ -552,6 +553,74 @@ fn main() {
552553
.display_order(0)
553554
)
554555
)
556+
.subcommand(
557+
Command::new("http-sync")
558+
.about("Manual sync")
559+
.arg(
560+
Arg::new("start-block")
561+
.long("start-block")
562+
.value_name("BLOCK_ID")
563+
.action(ArgAction::Set)
564+
.help("Block ID of source's head")
565+
.default_value("head")
566+
.required(true)
567+
.display_order(0)
568+
)
569+
.arg(
570+
Arg::new("source-url")
571+
.long("source-url")
572+
.value_name("URL")
573+
.action(ArgAction::Set)
574+
.help("URL to a synced beacon-API provider")
575+
.required(true)
576+
.display_order(0)
577+
)
578+
.arg(
579+
Arg::new("target-url")
580+
.long("target-url")
581+
.value_name("URL")
582+
.action(ArgAction::Set)
583+
.help("URL to an unsynced beacon-API provider")
584+
.required(true)
585+
.display_order(0)
586+
)
587+
.arg(
588+
Arg::new("testnet-dir")
589+
.short('d')
590+
.long("testnet-dir")
591+
.value_name("PATH")
592+
.action(ArgAction::Set)
593+
.global(true)
594+
.help("The testnet dir.")
595+
.display_order(0)
596+
)
597+
.arg(
598+
Arg::new("network")
599+
.long("network")
600+
.value_name("NAME")
601+
.action(ArgAction::Set)
602+
.global(true)
603+
.help("The network to use. Defaults to mainnet.")
604+
.conflicts_with("testnet-dir")
605+
.display_order(0)
606+
)
607+
.arg(
608+
Arg::new("known-common-ancestor")
609+
.long("known-common-ancestor")
610+
.value_name("BLOCK_ID")
611+
.action(ArgAction::Set)
612+
.help("Block ID of common ancestor, if known.")
613+
.display_order(0)
614+
)
615+
.arg(
616+
Arg::new("block-cache-dir")
617+
.long("block-cache-dir")
618+
.value_name("PATH")
619+
.action(ArgAction::Set)
620+
.help("Directory to keep a cache of the downloaded SSZ blocks.")
621+
.display_order(0)
622+
)
623+
)
555624
.get_matches();
556625

557626
let result = matches
@@ -656,6 +725,11 @@ fn run<E: EthSpec>(env_builder: EnvironmentBuilder<E>, matches: &ArgMatches) ->
656725
}
657726
Some(("mock-el", matches)) => mock_el::run::<E>(env, matches)
658727
.map_err(|e| format!("Failed to run mock-el command: {}", e)),
728+
Some(("http-sync", matches)) => {
729+
let network_config = get_network_config()?;
730+
http_sync::run::<E>(env, network_config, matches)
731+
.map_err(|e| format!("Failed to run http-sync command: {}", e))
732+
}
659733
Some((other, _)) => Err(format!("Unknown subcommand {}. See --help.", other)),
660734
_ => Err("No subcommand provided. See --help.".to_string()),
661735
}

0 commit comments

Comments
 (0)