1use std::{
2 collections::{BTreeMap, HashMap},
3 net::{IpAddr, SocketAddr},
4 num::NonZeroU32,
5 path::{Path, PathBuf},
6};
7
8use alloy_consensus::Sealable as _;
9use alloy_primitives::{Address, B256, Bytes, LogData, U256, keccak256};
10use commonware_codec::{Encode as _, EncodeSize, RangeCfg, Read, ReadExt, Write};
11use commonware_consensus::types::Epoch;
12use commonware_cryptography::{
13 bls12381::{
14 dkg::Output,
15 primitives::{group::Share, sharing::ModeVersion, variant::MinSig},
16 },
17 ed25519::PublicKey,
18 transcript::Summary,
19};
20use commonware_math::algebra::Random as _;
21use commonware_runtime::{Metrics as _, Runner as _};
22use commonware_storage::metadata::{Config as MetadataConfig, Metadata};
23use commonware_utils::{NZU32, ordered};
24use eyre::{Context as _, OptionExt as _, ensure, eyre};
25use rand_08::SeedableRng as _;
26use reth_db::{mdbx::DatabaseArguments, open_db};
27use reth_db_api::{
28 cursor::{DbCursorRO as _, DbCursorRW as _, DbDupCursorRO as _, DbDupCursorRW as _},
29 database::Database as _,
30 models::StorageSettings,
31 tables,
32 transaction::{DbTx, DbTxMut},
33};
34use reth_primitives_traits::StorageEntry;
35use reth_provider::{
36 BlockHashReader as _, HeaderProvider as _, StaticFileProviderBuilder, StaticFileSegment,
37 StaticFileWriter as _,
38};
39use revm::{
40 context::journaled_state::JournalCheckpoint,
41 state::{AccountInfo, Bytecode},
42};
43use serde::Deserialize;
44use tempo_chainspec::hardfork::TempoHardfork;
45use tempo_consensus_config::{SigningKey, SigningKeyPassphrase, SigningShare};
46use tempo_contracts::precompiles::VALIDATOR_CONFIG_V2_ADDRESS;
47use tempo_dkg_onchain_artifacts::OnchainDkgOutcome;
48use tempo_precompiles::{
49 error::TempoPrecompileError,
50 storage::{PrecompileStorageProvider, StorageCtx},
51 validator_config_v2::ValidatorConfigV2,
52};
53use tempo_primitives::TempoPrimitives;
54
55use crate::shadowfork::{
56 SHADOW_CHAINSPEC_FILE, SHADOW_EPOCH, SHADOWFORK_SIGNING_KEY_SECRET,
57 resolve_source_execution_data_dir, write_shadow_chainspec as write_shadow_chainspec_file,
58};
59
60const DKG_STATES_METADATA_PARTITION: &str = "engine_dkg_manager_states_metadata";
61const MAXIMUM_VALIDATORS: NonZeroU32 = NZU32!(u16::MAX as u32);
62
63#[derive(Debug, clap::Parser)]
65pub(crate) struct BootstrapShadowfork {
66 #[arg(long, value_name = "FILE")]
68 manifest: PathBuf,
69
70 #[arg(long, value_name = "INDEX")]
72 node_index: Option<usize>,
73
74 #[arg(long, value_name = "DIR")]
78 execution_datadir: Option<PathBuf>,
79
80 #[arg(long, value_name = "DIR", requires = "node_index")]
82 node_artifact_dir: Option<PathBuf>,
83
84 #[arg(long)]
86 force: bool,
87
88 #[arg(long, default_value_t = 0)]
90 seed: u64,
91}
92
93impl BootstrapShadowfork {
94 pub(crate) fn run(self) -> eyre::Result<()> {
95 let Self {
96 manifest,
97 node_index,
98 execution_datadir,
99 node_artifact_dir: node_artifact_dir_override,
100 force,
101 seed,
102 } = self;
103
104 let manifest_path = manifest;
105 let manifest_dir = manifest_path
106 .parent()
107 .filter(|path| !path.as_os_str().is_empty())
108 .unwrap_or_else(|| Path::new("."));
109 let manifest = read_manifest(&manifest_path)?;
110 let target_nodes = target_nodes(&manifest, node_index)?;
111 ensure!(
112 target_nodes.len() == 1,
113 "bootstrap-shadowfork patches one local execution datadir at a time; pass --node-index when the manifest contains multiple validators",
114 );
115 let shadow_epoch = manifest.shadow_epoch.unwrap_or(SHADOW_EPOCH);
116 ensure!(
117 shadow_epoch == SHADOW_EPOCH,
118 "unsupported shadow epoch `{}`; expected `{SHADOW_EPOCH}`",
119 shadow_epoch
120 );
121
122 let execution_datadir = execution_datadir
123 .or_else(|| manifest.source_execution_datadir.clone())
124 .ok_or_eyre(
125 "missing source execution datadir; pass --execution-datadir or regenerate the manifest",
126 )?;
127 let execution_datadir = resolve_source_execution_data_dir(
128 &execution_datadir,
129 &manifest.source_chain,
130 manifest.source_chain_id,
131 )?;
132
133 let reanchor_block_number = manifest.fork_block_number;
134 let fallback_shadow_epoch_length = reanchor_block_number
135 .checked_add(1)
136 .ok_or_eyre("fork block number overflowed shadow epoch length")?;
137 let shadow_epoch_length = fallback_shadow_epoch_length;
138 if let Some(manifest_shadow_epoch_length) = manifest.shadow_epoch_length
139 && manifest_shadow_epoch_length != shadow_epoch_length
140 {
141 eprintln!(
142 "warning: manifest shadow_epoch_length is `{manifest_shadow_epoch_length}`, but bootstrap needs `{shadow_epoch_length}` for the selected boundary block",
143 );
144 }
145
146 let chainspec_path =
147 write_shadow_chainspec(&manifest, manifest_dir, shadow_epoch_length, force)?;
148 let outcome = if let Some(outcome) = &manifest.shadow_dkg_outcome {
149 decode_outcome(outcome)?
150 } else {
151 read_private_genesis_outcome(manifest_dir)?
152 };
153
154 ensure!(
155 outcome.epoch == Epoch::new(SHADOW_EPOCH),
156 "shadow DKG outcome is for epoch `{}`, expected `{SHADOW_EPOCH}`",
157 outcome.epoch,
158 );
159
160 let shadow_validators = shadow_validator_registrations(&manifest)?;
161 let shadow_validator_config_v2_storage =
162 shadow_validator_config_v2_storage(&manifest, manifest_dir)?;
163 let target_node = target_nodes[0];
164 patch_execution_validator_registry(
165 &execution_datadir.db_path,
166 manifest.source_chain_id,
167 reanchor_block_number,
168 &shadow_validator_config_v2_storage,
169 &shadow_validators,
170 &outcome,
171 )
172 .wrap_err_with(|| {
173 format!(
174 "failed patching ValidatorConfigV2 in node-{} execution database `{}`",
175 target_node.index,
176 execution_datadir.db_path.display(),
177 )
178 })?;
179
180 for validator in &target_nodes {
181 let node_dir = node_artifact_dir(
182 manifest_dir,
183 validator,
184 node_artifact_dir_override.as_deref(),
185 );
186 let consensus_dir = node_dir.join("consensus");
187 let signing_key = SigningKey::read_from_file_encrypted(
188 node_dir.join("signing.key"),
189 SigningKeyPassphrase::from(SHADOWFORK_SIGNING_KEY_SECRET),
190 )
191 .wrap_err_with(|| {
192 format!(
193 "failed reading encrypted signing key for node-{}",
194 validator.index
195 )
196 })?;
197 let public_key: B256 = signing_key
198 .public_key()
199 .encode()
200 .as_ref()
201 .try_into()
202 .expect("ed25519 public keys are 32 bytes");
203 ensure!(
204 public_key == validator.validator_public_key,
205 "node-{} signing key public key `{}` does not match manifest public key `{}`",
206 validator.index,
207 public_key,
208 validator.validator_public_key,
209 );
210
211 let signing_share = SigningShare::read_from_file(node_dir.join("signing.share"))
212 .wrap_err_with(|| {
213 format!("failed reading signing share for node-{}", validator.index)
214 })?
215 .into_inner();
216
217 ensure!(
218 share_matches_outcome(&outcome, &signing_share),
219 "node-{} signing share does not match the generated shadow DKG outcome",
220 validator.index,
221 );
222
223 seed_consensus_state(
224 &consensus_dir,
225 outcome.clone(),
226 signing_share,
227 seed.saturating_add(validator.index as u64),
228 force,
229 )
230 .wrap_err_with(|| {
231 format!(
232 "failed seeding consensus state for node-{} at `{}`",
233 validator.index,
234 consensus_dir.display(),
235 )
236 })?;
237 }
238
239 println!("wrote shadow chainspec to `{}`", chainspec_path.display());
240 println!(
241 "seeded DKG state for {} validators at epoch {SHADOW_EPOCH}",
242 target_nodes.len()
243 );
244 println!(
245 "patched ValidatorConfigV2 in `{}`",
246 execution_datadir.db_path.display()
247 );
248 Ok(())
249 }
250}
251
252fn read_manifest(path: &Path) -> eyre::Result<ShadowForkManifest> {
253 let json = std::fs::read_to_string(path)
254 .wrap_err_with(|| format!("failed reading manifest `{}`", path.display()))?;
255 serde_json::from_str(&json)
256 .wrap_err_with(|| format!("failed parsing manifest `{}`", path.display()))
257}
258
259fn target_nodes(
260 manifest: &ShadowForkManifest,
261 node_index: Option<usize>,
262) -> eyre::Result<Vec<&NodeManifest>> {
263 if let Some(index) = node_index {
264 let node = manifest
265 .validators
266 .iter()
267 .find(|validator| validator.index == index)
268 .ok_or_else(|| eyre!("manifest does not contain node index `{index}`"))?;
269 Ok(vec![node])
270 } else {
271 Ok(manifest.validators.iter().collect())
272 }
273}
274
275fn node_artifact_dir(
276 manifest_dir: &Path,
277 node: &NodeManifest,
278 node_artifact_dir: Option<&Path>,
279) -> PathBuf {
280 if let Some(node_artifact_dir) = node_artifact_dir {
281 if node_artifact_dir.is_absolute() {
282 node_artifact_dir.to_path_buf()
283 } else {
284 manifest_dir.join(node_artifact_dir)
285 }
286 } else {
287 manifest_dir.join(format!("node-{}", node.index))
288 }
289}
290
291fn write_shadow_chainspec(
292 manifest: &ShadowForkManifest,
293 manifest_dir: &Path,
294 shadow_epoch_length: u64,
295 force: bool,
296) -> eyre::Result<PathBuf> {
297 let chainspec_path = manifest_dir.join(SHADOW_CHAINSPEC_FILE);
298 ensure!(
299 force || !chainspec_path.exists(),
300 "shadow chainspec `{}` already exists; rerun with --force to overwrite",
301 chainspec_path.display(),
302 );
303 write_shadow_chainspec_file(
304 &chainspec_path,
305 &manifest.source_chain,
306 manifest.source_chain_id,
307 shadow_epoch_length,
308 )?;
309 Ok(chainspec_path)
310}
311
312fn storage_key(slot: U256) -> B256 {
313 B256::from(slot.to_be_bytes::<32>())
314}
315
316fn parse_storage_word(value: &str) -> eyre::Result<U256> {
317 let bytes = const_hex::decode(value.trim_start_matches("0x"))?;
318 ensure!(
319 bytes.len() <= 32,
320 "storage word has {} bytes, expected at most 32",
321 bytes.len(),
322 );
323
324 let mut padded = [0u8; 32];
325 padded[32 - bytes.len()..].copy_from_slice(&bytes);
326 Ok(U256::from_be_bytes(padded))
327}
328
329fn read_storage_settings<TX: DbTx>(tx: &TX) -> eyre::Result<StorageSettings> {
330 let value = tx.get::<tables::Metadata>("storage_settings".to_string())?;
331 Ok(value
332 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
333 .unwrap_or_else(StorageSettings::v1))
334}
335
336type StorageSlot = (Address, U256);
337type StorageOverlay = HashMap<StorageSlot, U256>;
338type EmittedEvents = Vec<(Address, LogData)>;
339type StorageSnapshot = (StorageOverlay, EmittedEvents);
340
341struct DbStorageOverlay<'tx, TX> {
342 tx: &'tx TX,
343 chain_id: u64,
344 block_number: u64,
345 storage_settings: StorageSettings,
346 overlay: StorageOverlay,
347 events: EmittedEvents,
348 snapshots: Vec<StorageSnapshot>,
349}
350
351impl<'tx, TX> DbStorageOverlay<'tx, TX>
352where
353 TX: DbTx + DbTxMut,
354{
355 fn new(
356 tx: &'tx TX,
357 chain_id: u64,
358 block_number: u64,
359 storage_settings: StorageSettings,
360 ) -> Self {
361 Self {
362 tx,
363 chain_id,
364 block_number,
365 storage_settings,
366 overlay: HashMap::new(),
367 events: Vec::new(),
368 snapshots: Vec::new(),
369 }
370 }
371
372 fn read_db_storage(&self, address: Address, slot: U256) -> Result<U256, TempoPrecompileError> {
373 let key = storage_key(slot);
374 if self.storage_settings.use_hashed_state() {
375 let hashed_address = keccak256(address);
376 let hashed_key = keccak256(key);
377 let mut cursor = self
378 .tx
379 .cursor_dup_read::<tables::HashedStorages>()
380 .map_err(db_precompile_error)?;
381 if let Some(entry) = cursor
382 .seek_by_key_subkey(hashed_address, hashed_key)
383 .map_err(db_precompile_error)?
384 && entry.key == hashed_key
385 {
386 return Ok(entry.value);
387 }
388 } else {
389 let mut cursor = self
390 .tx
391 .cursor_dup_read::<tables::PlainStorageState>()
392 .map_err(db_precompile_error)?;
393 if let Some(entry) = cursor
394 .seek_by_key_subkey(address, key)
395 .map_err(db_precompile_error)?
396 && entry.key == key
397 {
398 return Ok(entry.value);
399 }
400 }
401 Ok(U256::ZERO)
402 }
403
404 fn read_db_account_info(&self, address: Address) -> Result<AccountInfo, TempoPrecompileError> {
405 let account = if self.storage_settings.use_hashed_state() {
406 self.tx
407 .get::<tables::HashedAccounts>(keccak256(address))
408 .map_err(db_precompile_error)?
409 } else {
410 self.tx
411 .get::<tables::PlainAccountState>(address)
412 .map_err(db_precompile_error)?
413 };
414
415 Ok(account.map(AccountInfo::from).unwrap_or_default())
416 }
417
418 fn clear_db_storage(&self, address: Address) -> eyre::Result<()> {
419 if self.storage_settings.use_hashed_state() {
420 let hashed_address = keccak256(address);
421 let mut cursor = self.tx.cursor_dup_write::<tables::HashedStorages>()?;
422 if cursor.seek_exact(hashed_address)?.is_some() {
423 cursor.delete_current_duplicates()?;
424 }
425 } else {
426 let mut cursor = self.tx.cursor_dup_write::<tables::PlainStorageState>()?;
427 if cursor.seek_exact(address)?.is_some() {
428 cursor.delete_current_duplicates()?;
429 }
430 }
431 Ok(())
432 }
433
434 fn write_db_storage(&self, address: Address, slot: U256, value: U256) -> eyre::Result<()> {
435 let key = storage_key(slot);
436 if self.storage_settings.use_hashed_state() {
437 let hashed_address = keccak256(address);
438 let hashed_key = keccak256(key);
439 let mut cursor = self.tx.cursor_dup_write::<tables::HashedStorages>()?;
440 if cursor
441 .seek_by_key_subkey(hashed_address, hashed_key)?
442 .is_some_and(|entry| entry.key == hashed_key)
443 {
444 cursor.delete_current()?;
445 }
446 if !value.is_zero() {
447 cursor.upsert(
448 hashed_address,
449 &StorageEntry {
450 key: hashed_key,
451 value,
452 },
453 )?;
454 }
455 } else {
456 let mut cursor = self.tx.cursor_dup_write::<tables::PlainStorageState>()?;
457 if cursor
458 .seek_by_key_subkey(address, key)?
459 .is_some_and(|entry| entry.key == key)
460 {
461 cursor.delete_current()?;
462 }
463 if !value.is_zero() {
464 cursor.upsert(address, &StorageEntry { key, value })?;
465 }
466 }
467 Ok(())
468 }
469}
470
471fn db_precompile_error(err: impl std::fmt::Display) -> TempoPrecompileError {
472 TempoPrecompileError::Fatal(err.to_string())
473}
474
475impl<TX> PrecompileStorageProvider for DbStorageOverlay<'_, TX>
476where
477 TX: DbTx + DbTxMut,
478{
479 fn chain_id(&self) -> u64 {
480 self.chain_id
481 }
482
483 fn timestamp(&self) -> U256 {
484 U256::ZERO
485 }
486
487 fn beneficiary(&self) -> Address {
488 Address::ZERO
489 }
490
491 fn block_number(&self) -> u64 {
492 self.block_number
493 }
494
495 fn set_code(&mut self, _address: Address, _code: Bytecode) -> Result<(), TempoPrecompileError> {
496 Ok(())
497 }
498
499 fn with_account_info(
500 &mut self,
501 address: Address,
502 f: &mut dyn FnMut(&AccountInfo),
503 ) -> Result<(), TempoPrecompileError> {
504 let account = self.read_db_account_info(address)?;
505 f(&account);
506 Ok(())
507 }
508
509 fn sload(&mut self, address: Address, key: U256) -> Result<U256, TempoPrecompileError> {
510 if let Some(value) = self.overlay.get(&(address, key)) {
511 return Ok(*value);
512 }
513 self.read_db_storage(address, key)
514 }
515
516 fn tload(&mut self, _address: Address, _key: U256) -> Result<U256, TempoPrecompileError> {
517 Ok(U256::ZERO)
518 }
519
520 fn sstore(
521 &mut self,
522 address: Address,
523 key: U256,
524 value: U256,
525 ) -> Result<(), TempoPrecompileError> {
526 self.overlay.insert((address, key), value);
527 Ok(())
528 }
529
530 fn tstore(
531 &mut self,
532 _address: Address,
533 _key: U256,
534 _value: U256,
535 ) -> Result<(), TempoPrecompileError> {
536 Ok(())
537 }
538
539 fn emit_event(&mut self, address: Address, event: LogData) -> Result<(), TempoPrecompileError> {
540 self.events.push((address, event));
541 Ok(())
542 }
543
544 fn deduct_gas(&mut self, _gas: u64) -> Result<(), TempoPrecompileError> {
545 Ok(())
546 }
547
548 fn refund_gas(&mut self, _gas: i64) {}
549
550 fn gas_limit(&self) -> u64 {
551 u64::MAX
552 }
553
554 fn gas_used(&self) -> u64 {
555 0
556 }
557
558 fn state_gas_used(&self) -> u64 {
559 0
560 }
561
562 fn gas_refunded(&self) -> i64 {
563 0
564 }
565
566 fn reservoir(&self) -> u64 {
567 0
568 }
569
570 fn spec(&self) -> TempoHardfork {
571 TempoHardfork::T2
572 }
573
574 fn amsterdam_eip8037_enabled(&self) -> bool {
575 false
576 }
577
578 fn is_static(&self) -> bool {
579 false
580 }
581
582 fn checkpoint(&mut self) -> JournalCheckpoint {
583 let idx = self.snapshots.len();
584 self.snapshots
585 .push((self.overlay.clone(), self.events.clone()));
586 JournalCheckpoint {
587 log_i: 0,
588 journal_i: idx,
589 selfdestructed_i: 0,
590 }
591 }
592
593 fn checkpoint_commit(&mut self, checkpoint: JournalCheckpoint) {
594 assert_eq!(
595 checkpoint.journal_i,
596 self.snapshots.len() - 1,
597 "out-of-order checkpoint commit",
598 );
599 self.snapshots.pop();
600 }
601
602 fn checkpoint_revert(&mut self, checkpoint: JournalCheckpoint) {
603 assert_eq!(
604 checkpoint.journal_i,
605 self.snapshots.len() - 1,
606 "out-of-order checkpoint revert",
607 );
608 let (overlay, events) = self.snapshots.remove(checkpoint.journal_i);
609 self.overlay = overlay;
610 self.events = events;
611 }
612
613 fn set_tip1060_storage_credits(&mut self, _enabled: bool) {
614 }
616}
617
618fn read_private_genesis_outcome(manifest_dir: &Path) -> eyre::Result<OnchainDkgOutcome> {
619 let genesis_path = manifest_dir.join("genesis.json");
620 let json = std::fs::read_to_string(&genesis_path)
621 .wrap_err_with(|| format!("failed reading `{}`", genesis_path.display()))?;
622 let genesis: serde_json::Value = serde_json::from_str(&json)
623 .wrap_err_with(|| format!("failed parsing `{}`", genesis_path.display()))?;
624 let extra_data = genesis
625 .get("extraData")
626 .and_then(serde_json::Value::as_str)
627 .ok_or_eyre("shadow genesis JSON does not contain string field `extraData`")?;
628 let mut outcome = decode_outcome(extra_data)?;
629 outcome.epoch = Epoch::new(SHADOW_EPOCH);
630 Ok(outcome)
631}
632
633fn decode_outcome(hex: &str) -> eyre::Result<OnchainDkgOutcome> {
634 let bytes = const_hex::decode(hex.trim_start_matches("0x"))
635 .wrap_err("failed decoding shadow_dkg_outcome hex")?;
636 OnchainDkgOutcome::read(&mut bytes.as_slice())
637 .wrap_err("failed decoding shadow_dkg_outcome payload")
638}
639
640#[derive(Clone, Debug)]
641struct ShadowValidatorRegistration {
642 index: usize,
643 public_key: B256,
644 validator_address: Address,
645 fee_recipient: Address,
646 ingress: SocketAddr,
647 egress: IpAddr,
648}
649
650fn shadow_validator_registrations(
651 manifest: &ShadowForkManifest,
652) -> eyre::Result<Vec<ShadowValidatorRegistration>> {
653 manifest
654 .validators
655 .iter()
656 .map(|validator| {
657 const_hex::decode(validator.validator_add_signature.trim_start_matches("0x"))
658 .wrap_err_with(|| {
659 format!(
660 "failed decoding addValidator signature for node-{}",
661 validator.index
662 )
663 })?;
664
665 Ok(ShadowValidatorRegistration {
666 index: validator.index,
667 public_key: validator.validator_public_key,
668 validator_address: validator.validator_address,
669 fee_recipient: validator.fee_recipient,
670 ingress: validator.validator_addr,
671 egress: validator.validator_addr.ip(),
672 })
673 })
674 .collect()
675}
676
677fn shadow_validator_config_v2_storage(
678 manifest: &ShadowForkManifest,
679 manifest_dir: &Path,
680) -> eyre::Result<Vec<(U256, U256)>> {
681 let storage = if let Some(storage) = &manifest.shadow_validator_config_v2_storage {
682 storage.clone()
683 } else {
684 read_generated_validator_config_v2_storage(manifest_dir)?
685 };
686
687 storage
688 .iter()
689 .map(|(slot, value)| {
690 Ok((
691 parse_storage_word(slot)
692 .wrap_err_with(|| format!("failed parsing ValidatorConfigV2 slot `{slot}`"))?,
693 parse_storage_word(value).wrap_err_with(|| {
694 format!("failed parsing ValidatorConfigV2 value at slot `{slot}`")
695 })?,
696 ))
697 })
698 .collect()
699}
700
701fn read_generated_validator_config_v2_storage(
702 manifest_dir: &Path,
703) -> eyre::Result<BTreeMap<String, String>> {
704 let genesis_path = manifest_dir.join("genesis.json");
705 let json = std::fs::read_to_string(&genesis_path)
706 .wrap_err_with(|| format!("failed reading `{}`", genesis_path.display()))?;
707 let genesis: serde_json::Value = serde_json::from_str(&json)
708 .wrap_err_with(|| format!("failed parsing `{}`", genesis_path.display()))?;
709
710 let registry_address = VALIDATOR_CONFIG_V2_ADDRESS.to_string().to_ascii_lowercase();
711 let storage = genesis
712 .get("alloc")
713 .and_then(serde_json::Value::as_object)
714 .and_then(|alloc| alloc.get(®istry_address))
715 .and_then(|account| account.get("storage"))
716 .and_then(serde_json::Value::as_object)
717 .ok_or_else(|| {
718 eyre!(
719 "generated genesis `{}` does not contain ValidatorConfigV2 storage at `{registry_address}`",
720 genesis_path.display(),
721 )
722 })?;
723
724 storage
725 .iter()
726 .map(|(slot, value)| {
727 Ok((
728 slot.clone(),
729 value
730 .as_str()
731 .ok_or_else(|| {
732 eyre!("generated ValidatorConfigV2 storage value is not a string")
733 })?
734 .to_string(),
735 ))
736 })
737 .collect()
738}
739
740fn patch_execution_validator_registry(
741 db_path: &Path,
742 chain_id: u64,
743 block_number: u64,
744 validator_config_storage: &[(U256, U256)],
745 validators: &[ShadowValidatorRegistration],
746 outcome: &OnchainDkgOutcome,
747) -> eyre::Result<()> {
748 ensure!(
749 db_path.exists(),
750 "execution database `{}` does not exist; pass --execution-datadir pointing at a stopped Tempo datadir, chain datadir, or db directory",
751 db_path.display(),
752 );
753
754 let db = open_db(db_path, DatabaseArguments::default())?;
755 let tx = db.tx_mut()?;
756 let storage_settings = read_storage_settings(&tx)?;
757 let mut storage = DbStorageOverlay::new(&tx, chain_id, block_number, storage_settings);
758
759 storage.clear_db_storage(VALIDATOR_CONFIG_V2_ADDRESS)?;
760 for &(slot, value) in validator_config_storage {
761 storage.write_db_storage(VALIDATOR_CONFIG_V2_ADDRESS, slot, value)?;
762 }
763
764 StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
765 let config = ValidatorConfigV2::default();
766 ensure!(
767 config.is_initialized()?,
768 "patched ValidatorConfigV2 is not initialized",
769 );
770 let active = config
771 .get_active_validators()
772 .wrap_err("patched ValidatorConfigV2 active set is unreadable")?;
773 ensure!(
774 active.len() == validators.len(),
775 "patched ValidatorConfigV2 active set has {} validators, expected {}",
776 active.len(),
777 validators.len(),
778 );
779
780 for validator in validators {
781 let current = config
782 .validator_by_public_key(validator.public_key)
783 .wrap_err_with(|| {
784 format!(
785 "patched ValidatorConfigV2 has no entry for node-{} public key `{}`",
786 validator.index, validator.public_key,
787 )
788 })?;
789 ensure!(
790 current.validatorAddress == validator.validator_address,
791 "node-{} public key has validator address `{}`, expected `{}`",
792 validator.index,
793 current.validatorAddress,
794 validator.validator_address,
795 );
796 ensure!(
797 current.feeRecipient == validator.fee_recipient,
798 "node-{} public key has fee recipient `{}`, expected `{}`",
799 validator.index,
800 current.feeRecipient,
801 validator.fee_recipient,
802 );
803 ensure!(
804 current.ingress == validator.ingress.to_string(),
805 "node-{} public key has ingress `{}`, expected `{}`",
806 validator.index,
807 current.ingress,
808 validator.ingress,
809 );
810 ensure!(
811 current.egress == validator.egress.to_string(),
812 "node-{} public key has egress `{}`, expected `{}`",
813 validator.index,
814 current.egress,
815 validator.egress,
816 );
817 ensure!(
818 current.deactivatedAtHeight == 0,
819 "node-{} public key is deactivated at height `{}`",
820 validator.index,
821 current.deactivatedAtHeight,
822 );
823 }
824
825 Ok(())
826 })?;
827
828 if let Some((old_hash, new_hash)) = patch_boundary_header(db_path, &tx, block_number, outcome)?
829 {
830 println!(
831 "patched boundary header at block {block_number} in `{}`: {old_hash} -> {new_hash}",
832 db_path.display(),
833 );
834 }
835 reanchor_execution_finality(&tx, block_number)?;
836
837 tx.commit()?;
838
839 println!(
840 "replaced ValidatorConfigV2 storage in `{}` with {} shadow validators",
841 db_path.display(),
842 validators.len(),
843 );
844 println!(
845 "reanchored execution safe/finalized block in `{}` to {block_number}",
846 db_path.display(),
847 );
848 Ok(())
849}
850
851fn reanchor_execution_finality<TX>(tx: &TX, block_number: u64) -> eyre::Result<()>
852where
853 TX: DbTx + DbTxMut,
854{
855 tx.put::<tables::ChainState>(tables::ChainStateKey::LastFinalizedBlock, block_number)
856 .wrap_err("failed reanchoring execution finalized block")?;
857 tx.put::<tables::ChainState>(tables::ChainStateKey::LastSafeBlock, block_number)
858 .wrap_err("failed reanchoring execution safe block")?;
859
860 ensure!(
861 tx.get::<tables::ChainState>(tables::ChainStateKey::LastFinalizedBlock)?
862 == Some(block_number),
863 "execution finalized block was not reanchored to `{block_number}`",
864 );
865 ensure!(
866 tx.get::<tables::ChainState>(tables::ChainStateKey::LastSafeBlock)? == Some(block_number),
867 "execution safe block was not reanchored to `{block_number}`",
868 );
869
870 Ok(())
871}
872
873fn patch_boundary_header<TX>(
874 db_path: &Path,
875 tx: &TX,
876 block_number: u64,
877 outcome: &OnchainDkgOutcome,
878) -> eyre::Result<Option<(B256, B256)>>
879where
880 TX: DbTx + DbTxMut,
881{
882 let static_files_path = execution_static_files_path(db_path)?;
883 ensure!(
884 static_files_path.exists(),
885 "execution static files directory `{}` does not exist; pass --execution-datadir pointing at a stopped Tempo datadir, chain datadir, or db directory",
886 static_files_path.display(),
887 );
888
889 let static_file_provider = StaticFileProviderBuilder::read_write(&static_files_path)
890 .build::<TempoPrimitives>()
891 .wrap_err_with(|| {
892 format!(
893 "failed opening execution static files at `{}`",
894 static_files_path.display(),
895 )
896 })?;
897 let highest_header = static_file_provider
898 .get_highest_static_file_block(StaticFileSegment::Headers)
899 .ok_or_else(|| eyre!("execution static files contain no headers"))?;
900 ensure!(
901 highest_header == block_number,
902 "shadow fork bootstrap can only patch a local execution datadir whose tip is the fork block. static files tip is `{highest_header}`, expected `{block_number}`. Stop the source node at the fork block or rerun generate-shadowfork against this datadir before bootstrapping.",
903 );
904
905 let mut header = static_file_provider
906 .header_by_number(block_number)
907 .wrap_err_with(|| format!("failed reading boundary header at block `{block_number}`"))?
908 .ok_or_else(|| eyre!("boundary header at block `{block_number}` was not found"))?;
909 let old_hash = static_file_provider
910 .block_hash(block_number)
911 .wrap_err_with(|| {
912 format!("failed reading canonical hash for boundary block `{block_number}`")
913 })?
914 .unwrap_or_else(|| header.hash_slow());
915
916 let extra_data = Bytes::from(outcome.encode());
917 if header.inner.extra_data == extra_data {
918 tx.delete::<tables::HeaderNumbers>(old_hash, None)?;
919 tx.put::<tables::HeaderNumbers>(old_hash, block_number)?;
920 verify_boundary_header_patch(
921 &static_files_path,
922 tx,
923 block_number,
924 old_hash,
925 None,
926 outcome,
927 )?;
928 println!(
929 "verified boundary header at block {block_number} in `{}` already contains the generated shadow DKG outcome",
930 db_path.display(),
931 );
932 return Ok(None);
933 }
934
935 header.inner.extra_data = extra_data;
936 let new_hash = header.hash_slow();
937 {
938 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
939 writer.prune_headers(1)?;
940 writer.commit()?;
941 }
942 {
943 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
944 writer.append_header(&header, &new_hash)?;
945 writer.commit()?;
946 }
947
948 tx.delete::<tables::HeaderNumbers>(old_hash, None)?;
949 tx.put::<tables::HeaderNumbers>(new_hash, block_number)?;
950
951 verify_boundary_header_patch(
952 &static_files_path,
953 tx,
954 block_number,
955 new_hash,
956 Some(old_hash),
957 outcome,
958 )?;
959
960 Ok(Some((old_hash, new_hash)))
961}
962
963fn verify_boundary_header_patch<TX>(
964 static_files_path: &Path,
965 tx: &TX,
966 block_number: u64,
967 expected_hash: B256,
968 previous_hash: Option<B256>,
969 expected_outcome: &OnchainDkgOutcome,
970) -> eyre::Result<()>
971where
972 TX: DbTx,
973{
974 let static_file_provider = StaticFileProviderBuilder::read_write(static_files_path)
975 .build::<TempoPrimitives>()
976 .wrap_err_with(|| {
977 format!(
978 "failed reopening execution static files at `{}` for boundary verification",
979 static_files_path.display(),
980 )
981 })?;
982
983 let header = static_file_provider
984 .header_by_number(block_number)
985 .wrap_err_with(|| {
986 format!("failed rereading patched boundary header at block `{block_number}`")
987 })?
988 .ok_or_else(|| eyre!("patched boundary header at block `{block_number}` was not found"))?;
989 let actual_hash = header.hash_slow();
990 ensure!(
991 actual_hash == expected_hash,
992 "patched boundary header at block `{block_number}` has hash `{actual_hash}`, expected `{expected_hash}`",
993 );
994
995 let canonical_hash = static_file_provider
996 .block_hash(block_number)
997 .wrap_err_with(|| {
998 format!("failed rereading canonical hash for boundary block `{block_number}`")
999 })?
1000 .ok_or_else(|| {
1001 eyre!("patched boundary block `{block_number}` has no canonical hash in static files")
1002 })?;
1003 ensure!(
1004 canonical_hash == expected_hash,
1005 "patched boundary block `{block_number}` has canonical hash `{canonical_hash}`, expected `{expected_hash}`",
1006 );
1007
1008 let decoded = OnchainDkgOutcome::read(&mut header.inner.extra_data.as_ref())
1009 .wrap_err("patched boundary header did not contain a valid generated shadow DKG outcome")?;
1010 ensure!(
1011 decoded.players() == expected_outcome.players()
1012 && decoded.next_players() == expected_outcome.next_players()
1013 && decoded.dealers() == expected_outcome.dealers(),
1014 "patched boundary header DKG peers do not match generated shadow validators: dealers={:?}, players={:?}, next_players={:?}, expected_dealers={:?}, expected_players={:?}, expected_next_players={:?}",
1015 decoded.dealers(),
1016 decoded.players(),
1017 decoded.next_players(),
1018 expected_outcome.dealers(),
1019 expected_outcome.players(),
1020 expected_outcome.next_players(),
1021 );
1022 ensure!(
1023 &decoded == expected_outcome,
1024 "patched boundary header DKG outcome does not match the generated shadow DKG outcome",
1025 );
1026
1027 let mapped_number = tx
1028 .get::<tables::HeaderNumbers>(expected_hash)
1029 .wrap_err_with(|| format!("failed reading HeaderNumbers entry for `{expected_hash}`"))?;
1030 ensure!(
1031 mapped_number == Some(block_number),
1032 "HeaderNumbers maps patched boundary hash `{expected_hash}` to `{:?}`, expected `{block_number}`",
1033 mapped_number,
1034 );
1035
1036 if let Some(previous_hash) = previous_hash.filter(|hash| *hash != expected_hash) {
1037 let mapped_number = tx
1038 .get::<tables::HeaderNumbers>(previous_hash)
1039 .wrap_err_with(|| {
1040 format!(
1041 "failed reading HeaderNumbers entry for old boundary hash `{previous_hash}`"
1042 )
1043 })?;
1044 ensure!(
1045 mapped_number.is_none(),
1046 "HeaderNumbers still maps old boundary hash `{previous_hash}` to `{:?}` after patching",
1047 mapped_number,
1048 );
1049 }
1050
1051 Ok(())
1052}
1053
1054fn execution_static_files_path(db_path: &Path) -> eyre::Result<PathBuf> {
1055 let chain_dir = db_path.parent().ok_or_else(|| {
1056 eyre!(
1057 "execution database path `{}` has no parent directory",
1058 db_path.display(),
1059 )
1060 })?;
1061 Ok(chain_dir.join("static_files"))
1062}
1063
1064fn share_matches_outcome(outcome: &OnchainDkgOutcome, share: &Share) -> bool {
1065 outcome
1066 .sharing()
1067 .partial_public(share.index)
1068 .is_ok_and(|partial| share.public::<MinSig>() == partial)
1069}
1070
1071fn seed_consensus_state(
1072 consensus_dir: &Path,
1073 outcome: OnchainDkgOutcome,
1074 signing_share: Share,
1075 seed: u64,
1076 force: bool,
1077) -> eyre::Result<()> {
1078 std::fs::create_dir_all(consensus_dir).wrap_err_with(|| {
1079 format!(
1080 "failed creating consensus directory `{}`",
1081 consensus_dir.display()
1082 )
1083 })?;
1084
1085 let consensus_dir = consensus_dir.to_path_buf();
1086 std::thread::Builder::new()
1087 .name("shadowfork-bootstrap-commonware".to_string())
1088 .spawn(move || {
1089 let runner = commonware_runtime::tokio::Runner::new(
1090 commonware_runtime::tokio::Config::default().with_storage_directory(consensus_dir),
1091 );
1092
1093 runner.start(|context| async move {
1094 let mut states = Metadata::<_, u64, BootstrapDkgState>::init(
1095 context.with_label("states"),
1096 MetadataConfig {
1097 partition: DKG_STATES_METADATA_PARTITION.to_string(),
1098 codec_config: MAXIMUM_VALIDATORS,
1099 },
1100 )
1101 .await
1102 .map_err(eyre::Report::from)
1103 .wrap_err("unable to initialize DKG states metadata")?;
1104
1105 let existing = states.keys().copied().collect::<Vec<_>>();
1106 ensure!(
1107 force || existing.is_empty(),
1108 "DKG states metadata already contains epochs {existing:?}; rerun with --force to overwrite",
1109 );
1110 if !existing.is_empty() {
1111 states.clear();
1112 }
1113
1114 let mut rng = rand_08::rngs::StdRng::seed_from_u64(seed);
1115 let state = BootstrapDkgState {
1116 epoch: outcome.epoch,
1117 seed: Summary::random(&mut rng),
1118 output: outcome.output,
1119 share: BootstrapShareState::Plaintext(Some(signing_share)),
1120 players: outcome.next_players,
1121 is_full_dkg: outcome.is_next_full_dkg,
1122 };
1123
1124 states
1125 .put_sync(SHADOW_EPOCH, state)
1126 .await
1127 .map_err(eyre::Report::from)
1128 .wrap_err("unable to write shadow DKG state metadata")
1129 })
1130 })
1131 .wrap_err("failed spawning commonware bootstrap thread")?
1132 .join()
1133 .map_err(|_| eyre!("commonware bootstrap thread panicked"))?
1134}
1135
1136#[derive(Debug, Deserialize)]
1137struct ShadowForkManifest {
1138 source_chain: String,
1139 source_chain_id: u64,
1140 source_execution_datadir: Option<PathBuf>,
1141 fork_block_number: u64,
1142 shadow_epoch: Option<u64>,
1143 shadow_epoch_length: Option<u64>,
1144 shadow_dkg_outcome: Option<String>,
1145 shadow_validator_config_v2_storage: Option<BTreeMap<String, String>>,
1146 validators: Vec<NodeManifest>,
1147}
1148
1149#[derive(Debug, Deserialize)]
1150struct NodeManifest {
1151 index: usize,
1152 validator_addr: SocketAddr,
1153 validator_public_key: B256,
1154 validator_address: Address,
1155 fee_recipient: Address,
1156 validator_add_signature: String,
1157}
1158
1159#[derive(Clone, Debug, PartialEq, Eq)]
1160enum BootstrapShareState {
1161 Plaintext(Option<Share>),
1162}
1163
1164impl EncodeSize for BootstrapShareState {
1165 fn encode_size(&self) -> usize {
1166 match self {
1167 Self::Plaintext(share) => 1 + share.encode_size(),
1168 }
1169 }
1170}
1171
1172impl Write for BootstrapShareState {
1173 fn write(&self, buf: &mut impl bytes::BufMut) {
1174 match self {
1175 Self::Plaintext(share) => {
1176 0u8.write(buf);
1177 share.write(buf);
1178 }
1179 }
1180 }
1181}
1182
1183impl Read for BootstrapShareState {
1184 type Cfg = ();
1185
1186 fn read_cfg(
1187 buf: &mut impl bytes::Buf,
1188 _cfg: &Self::Cfg,
1189 ) -> Result<Self, commonware_codec::Error> {
1190 let tag = u8::read(buf)?;
1191 match tag {
1192 0 => Ok(Self::Plaintext(ReadExt::read(buf)?)),
1193 other => Err(commonware_codec::Error::InvalidEnum(other)),
1194 }
1195 }
1196}
1197
1198#[derive(Clone, Debug, PartialEq, Eq)]
1199struct BootstrapDkgState {
1200 epoch: Epoch,
1201 seed: Summary,
1202 output: Output<MinSig, PublicKey>,
1203 share: BootstrapShareState,
1204 players: ordered::Set<PublicKey>,
1205 is_full_dkg: bool,
1206}
1207
1208impl BootstrapDkgState {
1209 fn legacy_syncers(&self) -> ordered::Set<PublicKey> {
1210 ordered::Set::default()
1211 }
1212}
1213
1214impl EncodeSize for BootstrapDkgState {
1215 fn encode_size(&self) -> usize {
1216 self.epoch.encode_size()
1217 + self.seed.encode_size()
1218 + self.output.encode_size()
1219 + self.share.encode_size()
1220 + self.players.encode_size()
1221 + self.legacy_syncers().encode_size()
1222 + self.is_full_dkg.encode_size()
1223 }
1224}
1225
1226impl Write for BootstrapDkgState {
1227 fn write(&self, buf: &mut impl bytes::BufMut) {
1228 self.epoch.write(buf);
1229 self.seed.write(buf);
1230 self.output.write(buf);
1231 self.share.write(buf);
1232 self.players.write(buf);
1233 self.legacy_syncers().write(buf);
1234 self.is_full_dkg.write(buf);
1235 }
1236}
1237
1238impl Read for BootstrapDkgState {
1239 type Cfg = NonZeroU32;
1240
1241 fn read_cfg(
1242 buf: &mut impl bytes::Buf,
1243 cfg: &Self::Cfg,
1244 ) -> Result<Self, commonware_codec::Error> {
1245 let epoch = ReadExt::read(buf)?;
1246 let seed = ReadExt::read(buf)?;
1247 let output = Read::read_cfg(buf, &(*cfg, ModeVersion::v0()))?;
1248 let share = ReadExt::read(buf)?;
1249 let players = Read::read_cfg(buf, &(RangeCfg::from(1..=(u16::MAX as usize)), ()))?;
1250 ordered::Set::<PublicKey>::read_cfg(buf, &(RangeCfg::from(0..=(u16::MAX as usize)), ()))?;
1251 let is_full_dkg = ReadExt::read(buf)?;
1252
1253 Ok(Self {
1254 epoch,
1255 seed,
1256 output,
1257 share,
1258 players,
1259 is_full_dkg,
1260 })
1261 }
1262}