Skip to main content

tempo_xtask/
bootstrap_shadowfork.rs

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/// Reanchors generated shadow-fork artifacts onto a local source execution datadir.
64#[derive(Debug, clap::Parser)]
65pub(crate) struct BootstrapShadowfork {
66    /// Path to the manifest written by `generate-shadowfork`.
67    #[arg(long, value_name = "FILE")]
68    manifest: PathBuf,
69
70    /// Bootstrap only this validator index.
71    #[arg(long, value_name = "INDEX")]
72    node_index: Option<usize>,
73
74    /// Source execution datadir, chain datadir, or db directory to patch.
75    ///
76    /// If omitted, uses the source execution datadir recorded by `generate-shadowfork`.
77    #[arg(long, value_name = "DIR")]
78    execution_datadir: Option<PathBuf>,
79
80    /// Override the generated node artifact directory for --node-index.
81    #[arg(long, value_name = "DIR", requires = "node_index")]
82    node_artifact_dir: Option<PathBuf>,
83
84    /// Overwrite an existing shadow chainspec or DKG state metadata.
85    #[arg(long)]
86    force: bool,
87
88    /// Deterministic seed for per-node DKG state randomness.
89    #[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        // DbStorageOverlay does not run TIP-1060 accounting.
615    }
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(&registry_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}