merge upstream, fix compile errors
This commit is contained in:
@@ -17,7 +17,6 @@ withdrawals-processing = [
|
||||
"beacon_chain/withdrawals-processing",
|
||||
"store/withdrawals-processing",
|
||||
"execution_layer/withdrawals-processing",
|
||||
"http_api/withdrawals-processing",
|
||||
]
|
||||
spec-minimal = ["beacon_chain/spec-minimal"]
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ participation_metrics = [] # Exposes validator participation metrics to Prometh
|
||||
fork_from_env = [] # Initialise the harness chain spec from the FORK_NAME env variable
|
||||
withdrawals-processing = [
|
||||
"state_processing/withdrawals-processing",
|
||||
"store/withdrawals-processing",
|
||||
"execution_layer/withdrawals-processing",
|
||||
"operation_pool/withdrawals-processing"
|
||||
]
|
||||
spec-minimal = ["kzg/minimal-spec"]
|
||||
|
||||
|
||||
@@ -366,7 +366,6 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
pub(crate) observed_attester_slashings:
|
||||
Mutex<ObservedOperations<AttesterSlashing<T::EthSpec>, T::EthSpec>>,
|
||||
/// Maintains a record of which validators we've seen BLS to execution changes for.
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
pub(crate) observed_bls_to_execution_changes:
|
||||
Mutex<ObservedOperations<SignedBlsToExecutionChange, T::EthSpec>>,
|
||||
/// The most recently validated light client finality update received on gossip.
|
||||
@@ -2293,29 +2292,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
&self,
|
||||
bls_to_execution_change: SignedBlsToExecutionChange,
|
||||
) -> Result<ObservationOutcome<SignedBlsToExecutionChange, T::EthSpec>, Error> {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
let current_fork = self.spec.fork_name_at_slot::<T::EthSpec>(self.slot()?);
|
||||
if let ForkName::Base | ForkName::Altair | ForkName::Merge = current_fork {
|
||||
// Disallow BLS to execution changes prior to the Capella fork.
|
||||
return Err(Error::BlsToExecutionChangeBadFork(current_fork));
|
||||
}
|
||||
|
||||
let wall_clock_state = self.wall_clock_state()?;
|
||||
|
||||
Ok(self
|
||||
.observed_bls_to_execution_changes
|
||||
.lock()
|
||||
.verify_and_observe(bls_to_execution_change, &wall_clock_state, &self.spec)?)
|
||||
let current_fork = self.spec.fork_name_at_slot::<T::EthSpec>(self.slot()?);
|
||||
if let ForkName::Base | ForkName::Altair | ForkName::Merge = current_fork {
|
||||
// Disallow BLS to execution changes prior to the Capella fork.
|
||||
return Err(Error::BlsToExecutionChangeBadFork(current_fork));
|
||||
}
|
||||
|
||||
// TODO: remove this whole block once withdrawals-processing is removed
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
{
|
||||
#[allow(clippy::drop_non_drop)]
|
||||
drop(bls_to_execution_change);
|
||||
Ok(ObservationOutcome::AlreadyKnown)
|
||||
}
|
||||
let wall_clock_state = self.wall_clock_state()?;
|
||||
|
||||
Ok(self
|
||||
.observed_bls_to_execution_changes
|
||||
.lock()
|
||||
.verify_and_observe(bls_to_execution_change, &wall_clock_state, &self.spec)?)
|
||||
}
|
||||
|
||||
/// Import a BLS to execution change to the op pool.
|
||||
@@ -2324,12 +2312,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
bls_to_execution_change: SigVerifiedOp<SignedBlsToExecutionChange, T::EthSpec>,
|
||||
) {
|
||||
if self.eth1_chain.is_some() {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
self.op_pool
|
||||
.insert_bls_to_execution_change(bls_to_execution_change);
|
||||
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
drop(bls_to_execution_change);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4879,9 +4863,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.ok_or(Error::InvalidSlot(prepare_slot))?
|
||||
.as_secs(),
|
||||
pre_payload_attributes.prev_randao,
|
||||
execution_layer
|
||||
.get_suggested_fee_recipient(proposer as u64)
|
||||
.await,
|
||||
execution_layer.get_suggested_fee_recipient(proposer).await,
|
||||
withdrawals,
|
||||
);
|
||||
|
||||
|
||||
@@ -815,7 +815,6 @@ where
|
||||
observed_voluntary_exits: <_>::default(),
|
||||
observed_proposer_slashings: <_>::default(),
|
||||
observed_attester_slashings: <_>::default(),
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
observed_bls_to_execution_changes: <_>::default(),
|
||||
latest_seen_finality_update: <_>::default(),
|
||||
latest_seen_optimistic_update: <_>::default(),
|
||||
|
||||
@@ -460,7 +460,7 @@ where
|
||||
if is_terminal_block_hash_set && !is_activation_epoch_reached {
|
||||
// Use the "empty" payload if there's a terminal block hash, but we haven't reached the
|
||||
// terminal block epoch yet.
|
||||
return Ok(BlockProposalContents::default_at_fork(fork));
|
||||
return BlockProposalContents::default_at_fork(fork).map_err(Into::into);
|
||||
}
|
||||
|
||||
let terminal_pow_block_hash = execution_layer
|
||||
@@ -473,7 +473,7 @@ where
|
||||
} else {
|
||||
// If the merge transition hasn't occurred yet and the EL hasn't found the terminal
|
||||
// block, return an "empty" payload.
|
||||
return Ok(BlockProposalContents::default_at_fork(fork));
|
||||
return BlockProposalContents::default_at_fork(fork).map_err(Into::into);
|
||||
}
|
||||
} else {
|
||||
latest_execution_payload_header_block_hash
|
||||
|
||||
@@ -402,7 +402,7 @@ impl<T: AggregateMap> NaiveAggregationPool<T> {
|
||||
|
||||
/// Returns the total number of items stored in `self`.
|
||||
pub fn num_items(&self) -> usize {
|
||||
self.maps.iter().map(|(_, map)| map.len()).sum()
|
||||
self.maps.values().map(T::len).sum()
|
||||
}
|
||||
|
||||
/// Returns an aggregated `T::Value` with the given `T::Data`, if any.
|
||||
@@ -448,11 +448,7 @@ impl<T: AggregateMap> NaiveAggregationPool<T> {
|
||||
// If we have too many maps, remove the lowest amount to ensure we only have
|
||||
// `SLOTS_RETAINED` left.
|
||||
if self.maps.len() > SLOTS_RETAINED {
|
||||
let mut slots = self
|
||||
.maps
|
||||
.iter()
|
||||
.map(|(slot, _map)| *slot)
|
||||
.collect::<Vec<_>>();
|
||||
let mut slots = self.maps.keys().copied().collect::<Vec<_>>();
|
||||
// Sort is generally pretty slow, however `SLOTS_RETAINED` is quite low so it should be
|
||||
// negligible.
|
||||
slots.sort_unstable();
|
||||
|
||||
@@ -6,12 +6,9 @@ use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use types::{
|
||||
AttesterSlashing, BeaconState, ChainSpec, EthSpec, ForkName, ProposerSlashing,
|
||||
SignedVoluntaryExit, Slot,
|
||||
SignedBlsToExecutionChange, SignedVoluntaryExit, Slot,
|
||||
};
|
||||
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
use types::SignedBlsToExecutionChange;
|
||||
|
||||
/// Number of validator indices to store on the stack in `observed_validators`.
|
||||
pub const SMALL_VEC_SIZE: usize = 8;
|
||||
|
||||
@@ -83,7 +80,6 @@ impl<E: EthSpec> ObservableOperation<E> for AttesterSlashing<E> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
impl<E: EthSpec> ObservableOperation<E> for SignedBlsToExecutionChange {
|
||||
fn observed_validators(&self) -> SmallVec<[u64; SMALL_VEC_SIZE]> {
|
||||
smallvec![self.message.validator_index]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Utilities for managing database schema changes.
|
||||
mod migration_schema_v12;
|
||||
mod migration_schema_v13;
|
||||
mod migration_schema_v14;
|
||||
|
||||
use crate::beacon_chain::{BeaconChainTypes, ETH1_CACHE_DB_KEY};
|
||||
use crate::eth1_chain::SszEth1;
|
||||
@@ -114,6 +115,14 @@ pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(SchemaVersion(13), SchemaVersion(14)) => {
|
||||
let ops = migration_schema_v14::upgrade_to_v14::<T>(db.clone(), log)?;
|
||||
db.store_schema_version_atomically(to, ops)
|
||||
}
|
||||
(SchemaVersion(14), SchemaVersion(13)) => {
|
||||
let ops = migration_schema_v14::downgrade_from_v14::<T>(db.clone(), log)?;
|
||||
db.store_schema_version_atomically(to, ops)
|
||||
}
|
||||
// Anything else is an error.
|
||||
(_, _) => Err(HotColdDBError::UnsupportedSchemaVersion {
|
||||
target_version: to,
|
||||
|
||||
@@ -168,16 +168,14 @@ pub fn downgrade_from_v12<T: BeaconChainTypes>(
|
||||
log: Logger,
|
||||
) -> Result<Vec<KeyValueStoreOp>, Error> {
|
||||
// Load a V12 op pool and transform it to V5.
|
||||
let PersistedOperationPoolV12 {
|
||||
let PersistedOperationPoolV12::<T::EthSpec> {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
} = if let Some(PersistedOperationPool::<T::EthSpec>::V12(op_pool)) =
|
||||
db.get_item(&OP_POOL_DB_KEY)?
|
||||
{
|
||||
op_pool
|
||||
} = if let Some(op_pool_v12) = db.get_item(&OP_POOL_DB_KEY)? {
|
||||
op_pool_v12
|
||||
} else {
|
||||
debug!(log, "Nothing to do, no operation pool stored");
|
||||
return Ok(vec![]);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY};
|
||||
use operation_pool::{
|
||||
PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV14,
|
||||
};
|
||||
use slog::{debug, info, Logger};
|
||||
use std::sync::Arc;
|
||||
use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem};
|
||||
|
||||
pub fn upgrade_to_v14<T: BeaconChainTypes>(
|
||||
db: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
log: Logger,
|
||||
) -> Result<Vec<KeyValueStoreOp>, Error> {
|
||||
// Load a V12 op pool and transform it to V14.
|
||||
let PersistedOperationPoolV12::<T::EthSpec> {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
} = if let Some(op_pool_v12) = db.get_item(&OP_POOL_DB_KEY)? {
|
||||
op_pool_v12
|
||||
} else {
|
||||
debug!(log, "Nothing to do, no operation pool stored");
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
// initialize with empty vector
|
||||
let bls_to_execution_changes = vec![];
|
||||
let v14 = PersistedOperationPool::V14(PersistedOperationPoolV14 {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
bls_to_execution_changes,
|
||||
});
|
||||
Ok(vec![v14.as_kv_store_op(OP_POOL_DB_KEY)])
|
||||
}
|
||||
|
||||
pub fn downgrade_from_v14<T: BeaconChainTypes>(
|
||||
db: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
log: Logger,
|
||||
) -> Result<Vec<KeyValueStoreOp>, Error> {
|
||||
// Load a V14 op pool and transform it to V12.
|
||||
let PersistedOperationPoolV14 {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
bls_to_execution_changes,
|
||||
} = if let Some(PersistedOperationPool::<T::EthSpec>::V14(op_pool)) =
|
||||
db.get_item(&OP_POOL_DB_KEY)?
|
||||
{
|
||||
op_pool
|
||||
} else {
|
||||
debug!(log, "Nothing to do, no operation pool stored");
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Dropping bls_to_execution_changes from pool";
|
||||
"count" => bls_to_execution_changes.len(),
|
||||
);
|
||||
|
||||
let v12 = PersistedOperationPoolV12 {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
};
|
||||
Ok(vec![v12.as_kv_store_op(OP_POOL_DB_KEY)])
|
||||
}
|
||||
@@ -11,11 +11,11 @@ use crate::{
|
||||
StateSkipConfig,
|
||||
};
|
||||
use bls::get_withdrawal_credentials;
|
||||
use execution_layer::test_utils::DEFAULT_JWT_SECRET;
|
||||
use execution_layer::{
|
||||
auth::JwtKey,
|
||||
test_utils::{
|
||||
ExecutionBlockGenerator, MockExecutionLayer, TestingBuilder, DEFAULT_TERMINAL_BLOCK,
|
||||
ExecutionBlockGenerator, MockExecutionLayer, TestingBuilder, DEFAULT_JWT_SECRET,
|
||||
DEFAULT_TERMINAL_BLOCK,
|
||||
},
|
||||
ExecutionLayer,
|
||||
};
|
||||
@@ -383,14 +383,43 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn recalculate_fork_times_with_genesis(mut self, genesis_time: u64) -> Self {
|
||||
let mock = self
|
||||
.mock_execution_layer
|
||||
.as_mut()
|
||||
.expect("must have mock execution layer to recalculate fork times");
|
||||
let spec = self
|
||||
.spec
|
||||
.clone()
|
||||
.expect("cannot recalculate fork times without spec");
|
||||
mock.server.execution_block_generator().shanghai_time =
|
||||
spec.capella_fork_epoch.map(|epoch| {
|
||||
genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
mock.server.execution_block_generator().eip4844_time =
|
||||
spec.eip4844_fork_epoch.map(|epoch| {
|
||||
genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mock_execution_layer(mut self) -> Self {
|
||||
let spec = self.spec.clone().expect("cannot build without spec");
|
||||
let shanghai_time = spec.capella_fork_epoch.map(|epoch| {
|
||||
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
let eip4844_time = spec.eip4844_fork_epoch.map(|epoch| {
|
||||
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
let mock = MockExecutionLayer::new(
|
||||
self.runtime.task_executor.clone(),
|
||||
spec.terminal_total_difficulty,
|
||||
DEFAULT_TERMINAL_BLOCK,
|
||||
spec.terminal_block_hash,
|
||||
spec.terminal_block_hash_activation_epoch,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||
None,
|
||||
);
|
||||
@@ -405,12 +434,20 @@ where
|
||||
let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
|
||||
|
||||
let spec = self.spec.clone().expect("cannot build without spec");
|
||||
let shanghai_time = spec.capella_fork_epoch.map(|epoch| {
|
||||
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
let eip4844_time = spec.eip4844_fork_epoch.map(|epoch| {
|
||||
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
|
||||
});
|
||||
let mock_el = MockExecutionLayer::new(
|
||||
self.runtime.task_executor.clone(),
|
||||
spec.terminal_total_difficulty,
|
||||
DEFAULT_TERMINAL_BLOCK,
|
||||
spec.terminal_block_hash,
|
||||
spec.terminal_block_hash_activation_epoch,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||
Some(builder_url.clone()),
|
||||
)
|
||||
@@ -1459,7 +1496,7 @@ where
|
||||
let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();
|
||||
|
||||
let signed_block = block.sign(
|
||||
&self.validator_keypairs[proposer_index as usize].sk,
|
||||
&self.validator_keypairs[proposer_index].sk,
|
||||
&state.fork(),
|
||||
state.genesis_validators_root(),
|
||||
&self.spec,
|
||||
|
||||
@@ -631,10 +631,7 @@ impl<T: EthSpec> ValidatorMonitor<T> {
|
||||
|
||||
// Return the `id`'s of all monitored validators.
|
||||
pub fn get_all_monitored_validators(&self) -> Vec<String> {
|
||||
self.validators
|
||||
.iter()
|
||||
.map(|(_, val)| val.id.clone())
|
||||
.collect()
|
||||
self.validators.values().map(|val| val.id.clone()).collect()
|
||||
}
|
||||
|
||||
/// If `self.auto_register == true`, add the `validator_index` to `self.monitored_validators`.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#![cfg(not(debug_assertions))] // Tests run too slow in debug.
|
||||
|
||||
use beacon_chain::test_utils::BeaconChainHarness;
|
||||
use execution_layer::test_utils::Block;
|
||||
use types::*;
|
||||
|
||||
const VALIDATOR_COUNT: usize = 32;
|
||||
type E = MainnetEthSpec;
|
||||
|
||||
fn verify_execution_payload_chain<T: EthSpec>(chain: &[FullPayload<T>]) {
|
||||
let mut prev_ep: Option<FullPayload<T>> = None;
|
||||
|
||||
for ep in chain {
|
||||
assert!(!ep.is_default_with_empty_roots());
|
||||
assert!(ep.block_hash() != ExecutionBlockHash::zero());
|
||||
|
||||
// Check against previous `ExecutionPayload`.
|
||||
if let Some(prev_ep) = prev_ep {
|
||||
assert_eq!(prev_ep.block_hash(), ep.execution_payload().parent_hash());
|
||||
assert_eq!(
|
||||
prev_ep.execution_payload().block_number() + 1,
|
||||
ep.execution_payload().block_number()
|
||||
);
|
||||
assert!(ep.execution_payload().timestamp() > prev_ep.execution_payload().timestamp());
|
||||
}
|
||||
prev_ep = Some(ep.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn base_altair_merge_capella() {
|
||||
let altair_fork_epoch = Epoch::new(4);
|
||||
let altair_fork_slot = altair_fork_epoch.start_slot(E::slots_per_epoch());
|
||||
let bellatrix_fork_epoch = Epoch::new(8);
|
||||
let merge_fork_slot = bellatrix_fork_epoch.start_slot(E::slots_per_epoch());
|
||||
let capella_fork_epoch = Epoch::new(12);
|
||||
let capella_fork_slot = capella_fork_epoch.start_slot(E::slots_per_epoch());
|
||||
|
||||
let mut spec = E::default_spec();
|
||||
spec.altair_fork_epoch = Some(altair_fork_epoch);
|
||||
spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch);
|
||||
spec.capella_fork_epoch = Some(capella_fork_epoch);
|
||||
|
||||
let harness = BeaconChainHarness::builder(E::default())
|
||||
.spec(spec)
|
||||
.logger(logging::test_logger())
|
||||
.deterministic_keypairs(VALIDATOR_COUNT)
|
||||
.fresh_ephemeral_store()
|
||||
.mock_execution_layer()
|
||||
.build();
|
||||
|
||||
/*
|
||||
* Start with the base fork.
|
||||
*/
|
||||
assert!(harness.chain.head_snapshot().beacon_block.as_base().is_ok());
|
||||
|
||||
/*
|
||||
* Do the Altair fork.
|
||||
*/
|
||||
harness.extend_to_slot(altair_fork_slot).await;
|
||||
|
||||
let altair_head = &harness.chain.head_snapshot().beacon_block;
|
||||
assert!(altair_head.as_altair().is_ok());
|
||||
assert_eq!(altair_head.slot(), altair_fork_slot);
|
||||
|
||||
/*
|
||||
* Do the merge fork, without a terminal PoW block.
|
||||
*/
|
||||
harness.extend_to_slot(merge_fork_slot).await;
|
||||
|
||||
let merge_head = &harness.chain.head_snapshot().beacon_block;
|
||||
assert!(merge_head.as_merge().is_ok());
|
||||
assert_eq!(merge_head.slot(), merge_fork_slot);
|
||||
assert!(
|
||||
merge_head
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.is_default_with_empty_roots(),
|
||||
"Merge head is default payload"
|
||||
);
|
||||
|
||||
/*
|
||||
* Next merge block shouldn't include an exec payload.
|
||||
*/
|
||||
harness.extend_slots(1).await;
|
||||
|
||||
let one_after_merge_head = &harness.chain.head_snapshot().beacon_block;
|
||||
assert!(
|
||||
one_after_merge_head
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.is_default_with_empty_roots(),
|
||||
"One after merge head is default payload"
|
||||
);
|
||||
assert_eq!(one_after_merge_head.slot(), merge_fork_slot + 1);
|
||||
|
||||
/*
|
||||
* Trigger the terminal PoW block.
|
||||
*/
|
||||
harness
|
||||
.execution_block_generator()
|
||||
.move_to_terminal_block()
|
||||
.unwrap();
|
||||
|
||||
// Add a slot duration to get to the next slot
|
||||
let timestamp = harness.get_timestamp_at_slot() + harness.spec.seconds_per_slot;
|
||||
harness
|
||||
.execution_block_generator()
|
||||
.modify_last_block(|block| {
|
||||
if let Block::PoW(terminal_block) = block {
|
||||
terminal_block.timestamp = timestamp;
|
||||
}
|
||||
});
|
||||
harness.extend_slots(1).await;
|
||||
|
||||
let two_after_merge_head = &harness.chain.head_snapshot().beacon_block;
|
||||
assert!(
|
||||
two_after_merge_head
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.is_default_with_empty_roots(),
|
||||
"Two after merge head is default payload"
|
||||
);
|
||||
assert_eq!(two_after_merge_head.slot(), merge_fork_slot + 2);
|
||||
|
||||
/*
|
||||
* Next merge block should include an exec payload.
|
||||
*/
|
||||
let mut execution_payloads = vec![];
|
||||
for _ in (merge_fork_slot.as_u64() + 3)..capella_fork_slot.as_u64() {
|
||||
harness.extend_slots(1).await;
|
||||
let block = &harness.chain.head_snapshot().beacon_block;
|
||||
let full_payload: FullPayload<E> = block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into();
|
||||
// pre-capella shouldn't have withdrawals
|
||||
assert!(full_payload.withdrawals_root().is_err());
|
||||
execution_payloads.push(full_payload);
|
||||
}
|
||||
|
||||
/*
|
||||
* Should enter capella fork now.
|
||||
*/
|
||||
for _ in 0..16 {
|
||||
harness.extend_slots(1).await;
|
||||
let block = &harness.chain.head_snapshot().beacon_block;
|
||||
let full_payload: FullPayload<E> = block
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into();
|
||||
// post-capella should have withdrawals
|
||||
assert!(full_payload.withdrawals_root().is_ok());
|
||||
execution_payloads.push(full_payload);
|
||||
}
|
||||
|
||||
verify_execution_payload_chain(execution_payloads.as_slice());
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod attestation_production;
|
||||
mod attestation_verification;
|
||||
mod block_verification;
|
||||
mod capella;
|
||||
mod merge;
|
||||
mod op_verification;
|
||||
mod payload_invalidation;
|
||||
|
||||
@@ -191,18 +191,17 @@ async fn base_altair_merge_with_terminal_block_after_fork() {
|
||||
|
||||
harness.extend_slots(1).await;
|
||||
|
||||
let one_after_merge_head = &harness.chain.head_snapshot().beacon_block;
|
||||
// FIXME: why is this being tested twice?
|
||||
let two_after_merge_head = &harness.chain.head_snapshot().beacon_block;
|
||||
assert!(
|
||||
one_after_merge_head
|
||||
two_after_merge_head
|
||||
.message()
|
||||
.body()
|
||||
.execution_payload()
|
||||
.unwrap()
|
||||
.is_default_with_empty_roots(),
|
||||
"One after merge head is default payload"
|
||||
"Two after merge head is default payload"
|
||||
);
|
||||
assert_eq!(one_after_merge_head.slot(), merge_fork_slot + 2);
|
||||
assert_eq!(two_after_merge_head.slot(), merge_fork_slot + 2);
|
||||
|
||||
/*
|
||||
* Next merge block should include an exec payload.
|
||||
|
||||
@@ -675,7 +675,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn test_finalization_boundaries() {
|
||||
let n = 8;
|
||||
let half = (n / 2) as usize;
|
||||
let half = n / 2;
|
||||
|
||||
let mut deposit_cache = get_cache_with_deposits(n as u64);
|
||||
|
||||
@@ -828,9 +828,9 @@ pub mod tests {
|
||||
// get_log(half+quarter) should return log with index `half+quarter`
|
||||
assert_eq!(
|
||||
q3_log_before_finalization.index,
|
||||
(half + quarter) as u64,
|
||||
half + quarter,
|
||||
"log index should be {}",
|
||||
(half + quarter),
|
||||
half + quarter,
|
||||
);
|
||||
|
||||
// get lower quarter of deposits with max deposit count
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
withdrawals-processing = ["state_processing/withdrawals-processing", "eth2/withdrawals-processing"]
|
||||
withdrawals-processing = ["state_processing/withdrawals-processing"]
|
||||
|
||||
[dependencies]
|
||||
types = { path = "../../consensus/types"}
|
||||
|
||||
@@ -27,7 +27,7 @@ impl From<jsonwebtoken::errors::Error> for Error {
|
||||
/// Provides wrapper around `[u8; JWT_SECRET_LENGTH]` that implements `Zeroize`.
|
||||
#[derive(Zeroize, Clone)]
|
||||
#[zeroize(drop)]
|
||||
pub struct JwtKey([u8; JWT_SECRET_LENGTH as usize]);
|
||||
pub struct JwtKey([u8; JWT_SECRET_LENGTH]);
|
||||
|
||||
impl JwtKey {
|
||||
/// Wrap given slice in `Self`. Returns an error if slice.len() != `JWT_SECRET_LENGTH`.
|
||||
|
||||
@@ -889,11 +889,11 @@ impl HttpJsonRpc {
|
||||
pub async fn supported_apis_v1(&self) -> Result<SupportedApis, Error> {
|
||||
Ok(SupportedApis {
|
||||
new_payload_v1: true,
|
||||
new_payload_v2: cfg!(feature = "withdrawals-processing"),
|
||||
new_payload_v2: cfg!(any(feature = "withdrawals-processing", test)),
|
||||
forkchoice_updated_v1: true,
|
||||
forkchoice_updated_v2: cfg!(feature = "withdrawals-processing"),
|
||||
forkchoice_updated_v2: cfg!(any(feature = "withdrawals-processing", test)),
|
||||
get_payload_v1: true,
|
||||
get_payload_v2: cfg!(feature = "withdrawals-processing"),
|
||||
get_payload_v2: cfg!(any(feature = "withdrawals-processing", test)),
|
||||
exchange_transition_configuration_v1: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,12 +348,14 @@ impl From<Withdrawal> for JsonWithdrawal {
|
||||
|
||||
impl From<JsonWithdrawal> for Withdrawal {
|
||||
fn from(jw: JsonWithdrawal) -> Self {
|
||||
// This comparison is done to avoid a scenario where the EE gives us too large a number and we
|
||||
// panic when attempting to cast to a `u64`.
|
||||
let amount = std::cmp::max(jw.amount / 1000000000, Uint256::from(u64::MAX));
|
||||
Self {
|
||||
index: jw.index,
|
||||
validator_index: jw.validator_index,
|
||||
address: jw.address,
|
||||
//FIXME(sean) if EE gives us too large a number this panics
|
||||
amount: (jw.amount / 1000000000).as_u64(),
|
||||
amount: amount.as_u64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use tokio::{
|
||||
time::sleep,
|
||||
};
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
use types::{AbstractExecPayload, Blob, ExecPayload, KzgCommitment};
|
||||
use types::{AbstractExecPayload, BeaconStateError, Blob, ExecPayload, KzgCommitment};
|
||||
use types::{
|
||||
BlindedPayload, BlockType, ChainSpec, Epoch, ExecutionBlockHash, ForkName,
|
||||
ProposerPreparationData, PublicKeyBytes, Signature, SignedBeaconBlock, Slot, Uint256,
|
||||
@@ -95,6 +95,13 @@ pub enum Error {
|
||||
FeeRecipientUnspecified,
|
||||
MissingLatestValidHash,
|
||||
InvalidJWTSecret(String),
|
||||
BeaconStateError(BeaconStateError),
|
||||
}
|
||||
|
||||
impl From<BeaconStateError> for Error {
|
||||
fn from(e: BeaconStateError) -> Self {
|
||||
Error::BeaconStateError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Error {
|
||||
@@ -150,17 +157,17 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
|
||||
} => payload,
|
||||
}
|
||||
}
|
||||
pub fn default_at_fork(fork_name: ForkName) -> Self {
|
||||
match fork_name {
|
||||
pub fn default_at_fork(fork_name: ForkName) -> Result<Self, BeaconStateError> {
|
||||
Ok(match fork_name {
|
||||
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
|
||||
BlockProposalContents::Payload(Payload::default_at_fork(fork_name))
|
||||
BlockProposalContents::Payload(Payload::default_at_fork(fork_name)?)
|
||||
}
|
||||
ForkName::Eip4844 => BlockProposalContents::PayloadAndBlobs {
|
||||
payload: Payload::default_at_fork(fork_name),
|
||||
payload: Payload::default_at_fork(fork_name)?,
|
||||
blobs: VariableList::default(),
|
||||
kzg_commitments: VariableList::default(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,10 +812,6 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
spec,
|
||||
) {
|
||||
Ok(()) => Ok(ProvenancedPayload::Builder(
|
||||
//FIXME(sean) the builder API needs to be updated
|
||||
// NOTE the comment above was removed in the
|
||||
// rebase with unstable.. I think it goes
|
||||
// here now?
|
||||
BlockProposalContents::Payload(relay.data.message.header),
|
||||
)),
|
||||
Err(reason) if !reason.payload_invalid() => {
|
||||
@@ -860,19 +863,11 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
||||
spec,
|
||||
) {
|
||||
Ok(()) => Ok(ProvenancedPayload::Builder(
|
||||
//FIXME(sean) the builder API needs to be updated
|
||||
// NOTE the comment above was removed in the
|
||||
// rebase with unstable.. I think it goes
|
||||
// here now?
|
||||
BlockProposalContents::Payload(relay.data.message.header),
|
||||
)),
|
||||
// If the payload is valid then use it. The local EE failed
|
||||
// to produce a payload so we have no alternative.
|
||||
Err(e) if !e.payload_invalid() => Ok(ProvenancedPayload::Builder(
|
||||
//FIXME(sean) the builder API needs to be updated
|
||||
// NOTE the comment above was removed in the
|
||||
// rebase with unstable.. I think it goes
|
||||
// here now?
|
||||
BlockProposalContents::Payload(relay.data.message.header),
|
||||
)),
|
||||
Err(reason) => {
|
||||
|
||||
@@ -13,7 +13,8 @@ use std::collections::HashMap;
|
||||
use tree_hash::TreeHash;
|
||||
use tree_hash_derive::TreeHash;
|
||||
use types::{
|
||||
EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadMerge, Hash256, Uint256,
|
||||
EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadCapella,
|
||||
ExecutionPayloadEip4844, ExecutionPayloadMerge, ForkName, Hash256, Uint256,
|
||||
};
|
||||
|
||||
const GAS_LIMIT: u64 = 16384;
|
||||
@@ -113,6 +114,11 @@ pub struct ExecutionBlockGenerator<T: EthSpec> {
|
||||
pub pending_payloads: HashMap<ExecutionBlockHash, ExecutionPayload<T>>,
|
||||
pub next_payload_id: u64,
|
||||
pub payload_ids: HashMap<PayloadId, ExecutionPayload<T>>,
|
||||
/*
|
||||
* Post-merge fork triggers
|
||||
*/
|
||||
pub shanghai_time: Option<u64>, // withdrawals
|
||||
pub eip4844_time: Option<u64>, // 4844
|
||||
}
|
||||
|
||||
impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
@@ -120,6 +126,8 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
terminal_total_difficulty: Uint256,
|
||||
terminal_block_number: u64,
|
||||
terminal_block_hash: ExecutionBlockHash,
|
||||
shanghai_time: Option<u64>,
|
||||
eip4844_time: Option<u64>,
|
||||
) -> Self {
|
||||
let mut gen = Self {
|
||||
head_block: <_>::default(),
|
||||
@@ -132,6 +140,8 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
pending_payloads: <_>::default(),
|
||||
next_payload_id: 0,
|
||||
payload_ids: <_>::default(),
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
};
|
||||
|
||||
gen.insert_pow_block(0).unwrap();
|
||||
@@ -163,6 +173,16 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fork_at_timestamp(&self, timestamp: u64) -> ForkName {
|
||||
match self.eip4844_time {
|
||||
Some(fork_time) if timestamp >= fork_time => ForkName::Eip4844,
|
||||
_ => match self.shanghai_time {
|
||||
Some(fork_time) if timestamp >= fork_time => ForkName::Capella,
|
||||
_ => ForkName::Merge,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execution_block_by_number(&self, number: u64) -> Option<ExecutionBlock> {
|
||||
self.block_by_number(number)
|
||||
.map(|block| block.as_execution_block(self.terminal_total_difficulty))
|
||||
@@ -395,7 +415,9 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forkchoice_updated_v1(
|
||||
// This function expects payload_attributes to already be validated with respect to
|
||||
// the current fork [obtained by self.get_fork_at_timestamp(payload_attributes.timestamp)]
|
||||
pub fn forkchoice_updated(
|
||||
&mut self,
|
||||
forkchoice_state: ForkchoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
@@ -469,23 +491,65 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
transactions: vec![].into(),
|
||||
}),
|
||||
PayloadAttributes::V2(pa) => {
|
||||
// FIXME: think about how to test different forks
|
||||
ExecutionPayload::Merge(ExecutionPayloadMerge {
|
||||
parent_hash: forkchoice_state.head_block_hash,
|
||||
fee_recipient: pa.suggested_fee_recipient,
|
||||
receipts_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
prev_randao: pa.prev_randao,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: GAS_LIMIT,
|
||||
gas_used: GAS_USED,
|
||||
timestamp: pa.timestamp,
|
||||
extra_data: "block gen was here".as_bytes().to_vec().into(),
|
||||
base_fee_per_gas: Uint256::one(),
|
||||
block_hash: ExecutionBlockHash::zero(),
|
||||
transactions: vec![].into(),
|
||||
})
|
||||
match self.get_fork_at_timestamp(pa.timestamp) {
|
||||
ForkName::Merge => ExecutionPayload::Merge(ExecutionPayloadMerge {
|
||||
parent_hash: forkchoice_state.head_block_hash,
|
||||
fee_recipient: pa.suggested_fee_recipient,
|
||||
receipts_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
prev_randao: pa.prev_randao,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: GAS_LIMIT,
|
||||
gas_used: GAS_USED,
|
||||
timestamp: pa.timestamp,
|
||||
extra_data: "block gen was here".as_bytes().to_vec().into(),
|
||||
base_fee_per_gas: Uint256::one(),
|
||||
block_hash: ExecutionBlockHash::zero(),
|
||||
transactions: vec![].into(),
|
||||
}),
|
||||
ForkName::Capella => {
|
||||
ExecutionPayload::Capella(ExecutionPayloadCapella {
|
||||
parent_hash: forkchoice_state.head_block_hash,
|
||||
fee_recipient: pa.suggested_fee_recipient,
|
||||
receipts_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
prev_randao: pa.prev_randao,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: GAS_LIMIT,
|
||||
gas_used: GAS_USED,
|
||||
timestamp: pa.timestamp,
|
||||
extra_data: "block gen was here".as_bytes().to_vec().into(),
|
||||
base_fee_per_gas: Uint256::one(),
|
||||
block_hash: ExecutionBlockHash::zero(),
|
||||
transactions: vec![].into(),
|
||||
withdrawals: pa.withdrawals.as_ref().unwrap().clone().into(),
|
||||
})
|
||||
}
|
||||
ForkName::Eip4844 => {
|
||||
ExecutionPayload::Eip4844(ExecutionPayloadEip4844 {
|
||||
parent_hash: forkchoice_state.head_block_hash,
|
||||
fee_recipient: pa.suggested_fee_recipient,
|
||||
receipts_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
prev_randao: pa.prev_randao,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: GAS_LIMIT,
|
||||
gas_used: GAS_USED,
|
||||
timestamp: pa.timestamp,
|
||||
extra_data: "block gen was here".as_bytes().to_vec().into(),
|
||||
base_fee_per_gas: Uint256::one(),
|
||||
// FIXME(4844): maybe this should be set to something?
|
||||
excess_data_gas: Uint256::one(),
|
||||
block_hash: ExecutionBlockHash::zero(),
|
||||
transactions: vec![].into(),
|
||||
withdrawals: pa.withdrawals.as_ref().unwrap().clone().into(),
|
||||
})
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -576,6 +640,8 @@ mod test {
|
||||
TERMINAL_DIFFICULTY.into(),
|
||||
TERMINAL_BLOCK,
|
||||
ExecutionBlockHash::zero(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
for i in 0..=TERMINAL_BLOCK {
|
||||
|
||||
@@ -82,17 +82,40 @@ pub async fn handle_rpc<T: EthSpec>(
|
||||
ENGINE_NEW_PAYLOAD_V2 => {
|
||||
JsonExecutionPayload::V2(get_param::<JsonExecutionPayloadV2<T>>(params, 0)?)
|
||||
}
|
||||
// TODO(4844) add that here..
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let fork = match request {
|
||||
JsonExecutionPayload::V1(_) => ForkName::Merge,
|
||||
JsonExecutionPayload::V2(ref payload) => {
|
||||
if payload.withdrawals.is_none() {
|
||||
ForkName::Merge
|
||||
} else {
|
||||
ForkName::Capella
|
||||
|
||||
let fork = ctx
|
||||
.execution_block_generator
|
||||
.read()
|
||||
.get_fork_at_timestamp(*request.timestamp());
|
||||
// validate method called correctly according to shanghai fork time
|
||||
match fork {
|
||||
ForkName::Merge => {
|
||||
if request.withdrawals().is_ok() && request.withdrawals().unwrap().is_some() {
|
||||
return Err(format!(
|
||||
"{} called with `withdrawals` before capella fork!",
|
||||
method
|
||||
));
|
||||
}
|
||||
}
|
||||
ForkName::Capella => {
|
||||
if method == ENGINE_NEW_PAYLOAD_V1 {
|
||||
return Err(format!("{} called after capella fork!", method));
|
||||
}
|
||||
if request.withdrawals().is_err()
|
||||
|| (request.withdrawals().is_ok()
|
||||
&& request.withdrawals().unwrap().is_none())
|
||||
{
|
||||
return Err(format!(
|
||||
"{} called without `withdrawals` after capella fork!",
|
||||
method
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO(4844) add 4844 error checking here
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Canned responses set by block hash take priority.
|
||||
@@ -125,7 +148,7 @@ pub async fn handle_rpc<T: EthSpec>(
|
||||
|
||||
Ok(serde_json::to_value(JsonPayloadStatusV1::from(response)).unwrap())
|
||||
}
|
||||
ENGINE_GET_PAYLOAD_V1 => {
|
||||
ENGINE_GET_PAYLOAD_V1 | ENGINE_GET_PAYLOAD_V2 => {
|
||||
let request: JsonPayloadIdRequest = get_param(params, 0)?;
|
||||
let id = request.into();
|
||||
|
||||
@@ -135,12 +158,76 @@ pub async fn handle_rpc<T: EthSpec>(
|
||||
.get_payload(&id)
|
||||
.ok_or_else(|| format!("no payload for id {:?}", id))?;
|
||||
|
||||
Ok(serde_json::to_value(JsonExecutionPayloadV1::try_from(response).unwrap()).unwrap())
|
||||
// validate method called correctly according to shanghai fork time
|
||||
if ctx
|
||||
.execution_block_generator
|
||||
.read()
|
||||
.get_fork_at_timestamp(response.timestamp())
|
||||
== ForkName::Capella
|
||||
&& method == ENGINE_GET_PAYLOAD_V1
|
||||
{
|
||||
return Err(format!("{} called after capella fork!", method));
|
||||
}
|
||||
// TODO(4844) add 4844 error checking here
|
||||
|
||||
match method {
|
||||
ENGINE_GET_PAYLOAD_V1 => Ok(serde_json::to_value(
|
||||
JsonExecutionPayloadV1::try_from(response).unwrap(),
|
||||
)
|
||||
.unwrap()),
|
||||
ENGINE_GET_PAYLOAD_V2 => Ok(serde_json::to_value(JsonGetPayloadResponse {
|
||||
execution_payload: JsonExecutionPayloadV2::try_from(response).unwrap(),
|
||||
})
|
||||
.unwrap()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
// FIXME(capella): handle fcu version 2
|
||||
ENGINE_FORKCHOICE_UPDATED_V1 => {
|
||||
ENGINE_FORKCHOICE_UPDATED_V1 | ENGINE_FORKCHOICE_UPDATED_V2 => {
|
||||
let forkchoice_state: JsonForkchoiceStateV1 = get_param(params, 0)?;
|
||||
let payload_attributes: Option<JsonPayloadAttributes> = get_param(params, 1)?;
|
||||
let payload_attributes = match method {
|
||||
ENGINE_FORKCHOICE_UPDATED_V1 => {
|
||||
let jpa1: Option<JsonPayloadAttributesV1> = get_param(params, 1)?;
|
||||
jpa1.map(JsonPayloadAttributes::V1)
|
||||
}
|
||||
ENGINE_FORKCHOICE_UPDATED_V2 => {
|
||||
let jpa2: Option<JsonPayloadAttributesV2> = get_param(params, 1)?;
|
||||
jpa2.map(JsonPayloadAttributes::V2)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// validate method called correctly according to shanghai fork time
|
||||
if let Some(pa) = payload_attributes.as_ref() {
|
||||
match ctx
|
||||
.execution_block_generator
|
||||
.read()
|
||||
.get_fork_at_timestamp(*pa.timestamp())
|
||||
{
|
||||
ForkName::Merge => {
|
||||
if pa.withdrawals().is_ok() && pa.withdrawals().unwrap().is_some() {
|
||||
return Err(format!(
|
||||
"{} called with `withdrawals` before capella fork!",
|
||||
method
|
||||
));
|
||||
}
|
||||
}
|
||||
ForkName::Capella => {
|
||||
if method == ENGINE_FORKCHOICE_UPDATED_V1 {
|
||||
return Err(format!("{} called after capella fork!", method));
|
||||
}
|
||||
if pa.withdrawals().is_err()
|
||||
|| (pa.withdrawals().is_ok() && pa.withdrawals().unwrap().is_none())
|
||||
{
|
||||
return Err(format!(
|
||||
"{} called without `withdrawals` after capella fork!",
|
||||
method
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO(4844) add 4844 error checking here
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(hook_response) = ctx
|
||||
.hook
|
||||
@@ -161,13 +248,10 @@ pub async fn handle_rpc<T: EthSpec>(
|
||||
return Ok(serde_json::to_value(response).unwrap());
|
||||
}
|
||||
|
||||
let mut response = ctx
|
||||
.execution_block_generator
|
||||
.write()
|
||||
.forkchoice_updated_v1(
|
||||
forkchoice_state.into(),
|
||||
payload_attributes.map(|json| json.into()),
|
||||
)?;
|
||||
let mut response = ctx.execution_block_generator.write().forkchoice_updated(
|
||||
forkchoice_state.into(),
|
||||
payload_attributes.map(|json| json.into()),
|
||||
)?;
|
||||
|
||||
if let Some(mut status) = ctx.static_forkchoice_updated_response.lock().clone() {
|
||||
if status.status == PayloadStatusV1Status::Valid {
|
||||
|
||||
@@ -26,17 +26,22 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
DEFAULT_TERMINAL_BLOCK,
|
||||
ExecutionBlockHash::zero(),
|
||||
Epoch::new(0),
|
||||
None,
|
||||
None,
|
||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
executor: TaskExecutor,
|
||||
terminal_total_difficulty: Uint256,
|
||||
terminal_block: u64,
|
||||
terminal_block_hash: ExecutionBlockHash,
|
||||
terminal_block_hash_activation_epoch: Epoch,
|
||||
shanghai_time: Option<u64>,
|
||||
eip4844_time: Option<u64>,
|
||||
jwt_key: Option<JwtKey>,
|
||||
builder_url: Option<SensitiveUrl>,
|
||||
) -> Self {
|
||||
@@ -54,6 +59,8 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
terminal_total_difficulty,
|
||||
terminal_block,
|
||||
terminal_block_hash,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
);
|
||||
|
||||
let url = SensitiveUrl::parse(&server.url()).unwrap();
|
||||
|
||||
@@ -45,6 +45,8 @@ pub struct MockExecutionConfig {
|
||||
pub terminal_difficulty: Uint256,
|
||||
pub terminal_block: u64,
|
||||
pub terminal_block_hash: ExecutionBlockHash,
|
||||
pub shanghai_time: Option<u64>,
|
||||
pub eip4844_time: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for MockExecutionConfig {
|
||||
@@ -55,6 +57,8 @@ impl Default for MockExecutionConfig {
|
||||
terminal_block: DEFAULT_TERMINAL_BLOCK,
|
||||
terminal_block_hash: ExecutionBlockHash::zero(),
|
||||
server_config: Config::default(),
|
||||
shanghai_time: None,
|
||||
eip4844_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +78,8 @@ impl<T: EthSpec> MockServer<T> {
|
||||
DEFAULT_TERMINAL_DIFFICULTY.into(),
|
||||
DEFAULT_TERMINAL_BLOCK,
|
||||
ExecutionBlockHash::zero(),
|
||||
None, // FIXME(capella): should this be the default?
|
||||
None, // FIXME(eip4844): should this be the default?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,11 +90,18 @@ impl<T: EthSpec> MockServer<T> {
|
||||
terminal_block,
|
||||
terminal_block_hash,
|
||||
server_config,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
} = config;
|
||||
let last_echo_request = Arc::new(RwLock::new(None));
|
||||
let preloaded_responses = Arc::new(Mutex::new(vec![]));
|
||||
let execution_block_generator =
|
||||
ExecutionBlockGenerator::new(terminal_difficulty, terminal_block, terminal_block_hash);
|
||||
let execution_block_generator = ExecutionBlockGenerator::new(
|
||||
terminal_difficulty,
|
||||
terminal_block,
|
||||
terminal_block_hash,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
);
|
||||
|
||||
let ctx: Arc<Context<T>> = Arc::new(Context {
|
||||
config: server_config,
|
||||
@@ -140,6 +153,8 @@ impl<T: EthSpec> MockServer<T> {
|
||||
terminal_difficulty: Uint256,
|
||||
terminal_block: u64,
|
||||
terminal_block_hash: ExecutionBlockHash,
|
||||
shanghai_time: Option<u64>,
|
||||
eip4844_time: Option<u64>,
|
||||
) -> Self {
|
||||
Self::new_with_config(
|
||||
handle,
|
||||
@@ -149,6 +164,8 @@ impl<T: EthSpec> MockServer<T> {
|
||||
terminal_difficulty,
|
||||
terminal_block,
|
||||
terminal_block_hash,
|
||||
shanghai_time,
|
||||
eip4844_time,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2021"
|
||||
autotests = false # using a single test binary compiles faster
|
||||
|
||||
[features]
|
||||
withdrawals-processing = []
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.3.2", features = ["tls"] }
|
||||
serde = { version = "1.0.116", features = ["derive"] }
|
||||
|
||||
@@ -216,10 +216,10 @@ impl BlockId {
|
||||
pub async fn blobs_sidecar<T: BeaconChainTypes>(
|
||||
&self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<(Arc<BlobsSidecar<T::EthSpec>>), warp::Rejection> {
|
||||
) -> Result<Arc<BlobsSidecar<T::EthSpec>>, warp::Rejection> {
|
||||
let root = self.root(chain)?.0;
|
||||
match chain.store.get_blobs(&root) {
|
||||
Ok(Some(blob)) => Ok((Arc::new(blob))),
|
||||
Ok(Some(blob)) => Ok(Arc::new(blob)),
|
||||
Ok(None) => Err(warp_utils::reject::custom_not_found(format!(
|
||||
"Blob with block root {} is not in the store",
|
||||
root
|
||||
|
||||
@@ -1681,16 +1681,12 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
|
||||
match chain.verify_bls_to_execution_change_for_gossip(address_change) {
|
||||
Ok(ObservationOutcome::New(verified_address_change)) => {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
publish_pubsub_message(
|
||||
&network_tx,
|
||||
PubsubMessage::BlsToExecutionChange(Box::new(
|
||||
verified_address_change.as_inner().clone(),
|
||||
)),
|
||||
)?;
|
||||
}
|
||||
|
||||
publish_pubsub_message(
|
||||
&network_tx,
|
||||
PubsubMessage::BlsToExecutionChange(Box::new(
|
||||
verified_address_change.as_inner().clone(),
|
||||
)),
|
||||
)?;
|
||||
chain.import_bls_to_execution_change(verified_address_change);
|
||||
}
|
||||
Ok(ObservationOutcome::AlreadyKnown) => {
|
||||
@@ -2915,7 +2911,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
let is_live =
|
||||
chain.validator_seen_at_epoch(index as usize, request_data.epoch);
|
||||
api_types::LivenessResponseData {
|
||||
index: index as u64,
|
||||
index,
|
||||
epoch: request_data.epoch,
|
||||
is_live,
|
||||
}
|
||||
@@ -2951,7 +2947,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.and_then(
|
||||
|sysinfo, app_start: std::time::Instant, data_dir, network_globals| {
|
||||
blocking_json_task(move || {
|
||||
let app_uptime = app_start.elapsed().as_secs() as u64;
|
||||
let app_uptime = app_start.elapsed().as_secs();
|
||||
Ok(api_types::GenericResponse::from(observe_system_health_bn(
|
||||
sysinfo,
|
||||
data_dir,
|
||||
|
||||
@@ -194,6 +194,11 @@ async fn reconstruct_block<T: BeaconChainTypes>(
|
||||
.spec
|
||||
.fork_name_at_epoch(block.slot().epoch(T::EthSpec::slots_per_epoch())),
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"Default payload construction error: {e:?}"
|
||||
))
|
||||
})?
|
||||
.into()
|
||||
// If we already have an execution payload with this transactions root cached, use it.
|
||||
} else if let Some(cached_payload) =
|
||||
|
||||
@@ -186,14 +186,7 @@ impl RealScore {
|
||||
|
||||
/// Add an f64 to the score abiding by the limits.
|
||||
fn add(&mut self, score: f64) {
|
||||
let mut new_score = self.lighthouse_score + score;
|
||||
if new_score > MAX_SCORE {
|
||||
new_score = MAX_SCORE;
|
||||
}
|
||||
if new_score < MIN_SCORE {
|
||||
new_score = MIN_SCORE;
|
||||
}
|
||||
|
||||
let new_score = (self.lighthouse_score + score).clamp(MIN_SCORE, MAX_SCORE);
|
||||
self.set_lighthouse_score(new_score);
|
||||
}
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ fn handle_length(
|
||||
// Note: length-prefix of > 10 bytes(uint64) would be a decoding error
|
||||
match uvi_codec.decode(bytes).map_err(RPCError::from)? {
|
||||
Some(length) => {
|
||||
*len = Some(length as usize);
|
||||
*len = Some(length);
|
||||
Ok(Some(length))
|
||||
}
|
||||
None => Ok(None), // need more bytes to decode length
|
||||
|
||||
@@ -119,8 +119,8 @@ lazy_static! {
|
||||
pub(crate) const MAX_RPC_SIZE: usize = 1_048_576; // 1M
|
||||
/// The maximum bytes that can be sent across the RPC post-merge.
|
||||
pub(crate) const MAX_RPC_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M
|
||||
//FIXME(sean) should these be the same?
|
||||
pub(crate) const MAX_RPC_SIZE_POST_CAPELLA: usize = 10 * 1_048_576; // 10M
|
||||
// FIXME(sean) should this be increased to account for blobs?
|
||||
pub(crate) const MAX_RPC_SIZE_POST_EIP4844: usize = 10 * 1_048_576; // 10M
|
||||
/// The protocol prefix the RPC protocol id.
|
||||
const PROTOCOL_PREFIX: &str = "/eth2/beacon_chain/req";
|
||||
|
||||
@@ -270,11 +270,11 @@ impl<TSpec: EthSpec> PeerScoreSettings<TSpec> {
|
||||
|
||||
let modulo_smaller = max(
|
||||
1,
|
||||
smaller_committee_size / self.target_aggregators_per_committee as usize,
|
||||
smaller_committee_size / self.target_aggregators_per_committee,
|
||||
);
|
||||
let modulo_larger = max(
|
||||
1,
|
||||
(smaller_committee_size + 1) / self.target_aggregators_per_committee as usize,
|
||||
(smaller_committee_size + 1) / self.target_aggregators_per_committee,
|
||||
);
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -88,7 +88,7 @@ fn keypair_from_hex(hex_bytes: &str) -> error::Result<Keypair> {
|
||||
hex_bytes.to_string()
|
||||
};
|
||||
|
||||
hex::decode(&hex_bytes)
|
||||
hex::decode(hex_bytes)
|
||||
.map_err(|e| format!("Failed to parse p2p secret key bytes: {:?}", e).into())
|
||||
.and_then(keypair_from_bytes)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,8 @@ const MAX_AGGREGATED_ATTESTATION_REPROCESS_QUEUE_LEN: usize = 1_024;
|
||||
/// before we start dropping them.
|
||||
const MAX_GOSSIP_BLOCK_QUEUE_LEN: usize = 1_024;
|
||||
|
||||
//FIXME(sean) verify
|
||||
/// The maximum number of queued `SignedBeaconBlockAndBlobsSidecar` objects received on gossip that
|
||||
/// will be stored before we start dropping them.
|
||||
const MAX_GOSSIP_BLOCK_AND_BLOB_QUEUE_LEN: usize = 1_024;
|
||||
|
||||
/// The maximum number of queued `SignedBeaconBlock` objects received prior to their slot (but
|
||||
@@ -1213,7 +1214,6 @@ impl<T: BeaconChainTypes> BeaconProcessor<T> {
|
||||
// required to verify some attestations.
|
||||
} else if let Some(item) = gossip_block_queue.pop() {
|
||||
self.spawn_worker(item, toolbox);
|
||||
//FIXME(sean)
|
||||
} else if let Some(item) = gossip_block_and_blobs_sidecar_queue.pop() {
|
||||
self.spawn_worker(item, toolbox);
|
||||
// Check the aggregates, *then* the unaggregates since we assume that
|
||||
|
||||
@@ -4,9 +4,6 @@ version = "0.2.0"
|
||||
authors = ["Michael Sproul <michael@sigmaprime.io>"]
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
withdrawals-processing = []
|
||||
|
||||
[dependencies]
|
||||
derivative = "2.1.1"
|
||||
itertools = "0.10.0"
|
||||
|
||||
@@ -49,7 +49,7 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> {
|
||||
let indices = get_attesting_indices::<T>(committee.committee, &fresh_validators).ok()?;
|
||||
let fresh_validators_rewards: HashMap<u64, u64> = indices
|
||||
.iter()
|
||||
.map(|i| *i as u64)
|
||||
.copied()
|
||||
.flat_map(|validator_index| {
|
||||
let reward = base::get_base_reward(
|
||||
state,
|
||||
|
||||
@@ -12,7 +12,8 @@ pub use attestation::AttMaxCover;
|
||||
pub use attestation_storage::{AttestationRef, SplitAttestation};
|
||||
pub use max_cover::MaxCover;
|
||||
pub use persistence::{
|
||||
PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV5,
|
||||
PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV14,
|
||||
PersistedOperationPoolV5,
|
||||
};
|
||||
pub use reward_cache::RewardCache;
|
||||
|
||||
@@ -51,7 +52,6 @@ pub struct OperationPool<T: EthSpec + Default> {
|
||||
/// Map from exiting validator to their exit data.
|
||||
voluntary_exits: RwLock<HashMap<u64, SigVerifiedOp<SignedVoluntaryExit, T>>>,
|
||||
/// Map from credential changing validator to their execution change data.
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
bls_to_execution_changes: RwLock<HashMap<u64, SigVerifiedOp<SignedBlsToExecutionChange, T>>>,
|
||||
/// Reward cache for accelerating attestation packing.
|
||||
reward_cache: RwLock<RewardCache>,
|
||||
@@ -518,17 +518,10 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
&self,
|
||||
verified_change: SigVerifiedOp<SignedBlsToExecutionChange, T>,
|
||||
) {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
self.bls_to_execution_changes.write().insert(
|
||||
verified_change.as_inner().message.validator_index,
|
||||
verified_change,
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
{
|
||||
drop(verified_change);
|
||||
}
|
||||
self.bls_to_execution_changes.write().insert(
|
||||
verified_change.as_inner().message.validator_index,
|
||||
verified_change,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get a list of execution changes for inclusion in a block.
|
||||
@@ -539,32 +532,19 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
state: &BeaconState<T>,
|
||||
spec: &ChainSpec,
|
||||
) -> Vec<SignedBlsToExecutionChange> {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
filter_limit_operations(
|
||||
self.bls_to_execution_changes.read().values(),
|
||||
|address_change| {
|
||||
address_change.signature_is_still_valid(&state.fork())
|
||||
&& state
|
||||
.get_validator(
|
||||
address_change.as_inner().message.validator_index as usize,
|
||||
)
|
||||
.map_or(false, |validator| {
|
||||
!validator.has_eth1_withdrawal_credential(spec)
|
||||
})
|
||||
},
|
||||
|address_change| address_change.as_inner().clone(),
|
||||
T::MaxBlsToExecutionChanges::to_usize(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: remove this whole block once withdrwals-processing is removed
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
{
|
||||
#[allow(clippy::drop_copy)]
|
||||
drop((state, spec));
|
||||
vec![]
|
||||
}
|
||||
filter_limit_operations(
|
||||
self.bls_to_execution_changes.read().values(),
|
||||
|address_change| {
|
||||
address_change.signature_is_still_valid(&state.fork())
|
||||
&& state
|
||||
.get_validator(address_change.as_inner().message.validator_index as usize)
|
||||
.map_or(false, |validator| {
|
||||
!validator.has_eth1_withdrawal_credential(spec)
|
||||
})
|
||||
},
|
||||
|address_change| address_change.as_inner().clone(),
|
||||
T::MaxBlsToExecutionChanges::to_usize(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Prune BLS to execution changes that have been applied to the state more than 1 block ago.
|
||||
@@ -579,32 +559,22 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
head_state: &BeaconState<T>,
|
||||
spec: &ChainSpec,
|
||||
) {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
prune_validator_hash_map(
|
||||
&mut self.bls_to_execution_changes.write(),
|
||||
|validator_index, validator| {
|
||||
validator.has_eth1_withdrawal_credential(spec)
|
||||
&& head_block
|
||||
.message()
|
||||
.body()
|
||||
.bls_to_execution_changes()
|
||||
.map_or(true, |recent_changes| {
|
||||
!recent_changes
|
||||
.iter()
|
||||
.any(|c| c.message.validator_index == validator_index)
|
||||
})
|
||||
},
|
||||
head_state,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove this whole block once withdrwals-processing is removed
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
{
|
||||
#[allow(clippy::drop_copy)]
|
||||
drop((head_block, head_state, spec));
|
||||
}
|
||||
prune_validator_hash_map(
|
||||
&mut self.bls_to_execution_changes.write(),
|
||||
|validator_index, validator| {
|
||||
validator.has_eth1_withdrawal_credential(spec)
|
||||
&& head_block
|
||||
.message()
|
||||
.body()
|
||||
.bls_to_execution_changes()
|
||||
.map_or(true, |recent_changes| {
|
||||
!recent_changes
|
||||
.iter()
|
||||
.any(|c| c.message.validator_index == validator_index)
|
||||
})
|
||||
},
|
||||
head_state,
|
||||
);
|
||||
}
|
||||
|
||||
/// Prune all types of transactions given the latest head state and head fork.
|
||||
@@ -691,17 +661,11 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
///
|
||||
/// This method may return objects that are invalid for block inclusion.
|
||||
pub fn get_all_bls_to_execution_changes(&self) -> Vec<SignedBlsToExecutionChange> {
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
{
|
||||
self.bls_to_execution_changes
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, address_change)| address_change.as_inner().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "withdrawals-processing"))]
|
||||
vec![]
|
||||
self.bls_to_execution_changes
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, address_change)| address_change.as_inner().clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1787,7 +1751,7 @@ mod release_tests {
|
||||
|
||||
fn cross_fork_harness<E: EthSpec>() -> (BeaconChainHarness<EphemeralHarnessType<E>>, ChainSpec)
|
||||
{
|
||||
let mut spec = test_spec::<E>();
|
||||
let mut spec = E::default_spec();
|
||||
|
||||
// Give some room to sign surround slashings.
|
||||
spec.altair_fork_epoch = Some(Epoch::new(3));
|
||||
|
||||
@@ -18,7 +18,7 @@ type PersistedSyncContributions<T> = Vec<(SyncAggregateId, Vec<SyncCommitteeCont
|
||||
/// Operations are stored in arbitrary order, so it's not a good idea to compare instances
|
||||
/// of this type (or its encoded form) for equality. Convert back to an `OperationPool` first.
|
||||
#[superstruct(
|
||||
variants(V5, V12),
|
||||
variants(V5, V12, V14),
|
||||
variant_attributes(
|
||||
derive(Derivative, PartialEq, Debug, Encode, Decode),
|
||||
derivative(Clone),
|
||||
@@ -32,7 +32,7 @@ pub struct PersistedOperationPool<T: EthSpec> {
|
||||
#[superstruct(only(V5))]
|
||||
pub attestations_v5: Vec<(AttestationId, Vec<Attestation<T>>)>,
|
||||
/// Attestations and their attesting indices.
|
||||
#[superstruct(only(V12))]
|
||||
#[superstruct(only(V12, V14))]
|
||||
pub attestations: Vec<(Attestation<T>, Vec<u64>)>,
|
||||
/// Mapping from sync contribution ID to sync contributions and aggregate.
|
||||
pub sync_contributions: PersistedSyncContributions<T>,
|
||||
@@ -40,20 +40,23 @@ pub struct PersistedOperationPool<T: EthSpec> {
|
||||
#[superstruct(only(V5))]
|
||||
pub attester_slashings_v5: Vec<(AttesterSlashing<T>, ForkVersion)>,
|
||||
/// Attester slashings.
|
||||
#[superstruct(only(V12))]
|
||||
#[superstruct(only(V12, V14))]
|
||||
pub attester_slashings: Vec<SigVerifiedOp<AttesterSlashing<T>, T>>,
|
||||
/// [DEPRECATED] Proposer slashings.
|
||||
#[superstruct(only(V5))]
|
||||
pub proposer_slashings_v5: Vec<ProposerSlashing>,
|
||||
/// Proposer slashings with fork information.
|
||||
#[superstruct(only(V12))]
|
||||
#[superstruct(only(V12, V14))]
|
||||
pub proposer_slashings: Vec<SigVerifiedOp<ProposerSlashing, T>>,
|
||||
/// [DEPRECATED] Voluntary exits.
|
||||
#[superstruct(only(V5))]
|
||||
pub voluntary_exits_v5: Vec<SignedVoluntaryExit>,
|
||||
/// Voluntary exits with fork information.
|
||||
#[superstruct(only(V12))]
|
||||
#[superstruct(only(V12, V14))]
|
||||
pub voluntary_exits: Vec<SigVerifiedOp<SignedVoluntaryExit, T>>,
|
||||
/// BLS to Execution Changes
|
||||
#[superstruct(only(V14))]
|
||||
pub bls_to_execution_changes: Vec<SigVerifiedOp<SignedBlsToExecutionChange, T>>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> PersistedOperationPool<T> {
|
||||
@@ -99,12 +102,20 @@ impl<T: EthSpec> PersistedOperationPool<T> {
|
||||
.map(|(_, exit)| exit.clone())
|
||||
.collect();
|
||||
|
||||
PersistedOperationPool::V12(PersistedOperationPoolV12 {
|
||||
let bls_to_execution_changes = operation_pool
|
||||
.bls_to_execution_changes
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, bls_to_execution_change)| bls_to_execution_change.clone())
|
||||
.collect();
|
||||
|
||||
PersistedOperationPool::V14(PersistedOperationPoolV14 {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
bls_to_execution_changes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,24 +138,41 @@ impl<T: EthSpec> PersistedOperationPool<T> {
|
||||
);
|
||||
let sync_contributions = RwLock::new(self.sync_contributions().iter().cloned().collect());
|
||||
let attestations = match self {
|
||||
PersistedOperationPool::V5(_) => return Err(OpPoolError::IncorrectOpPoolVariant),
|
||||
PersistedOperationPool::V12(pool) => {
|
||||
PersistedOperationPool::V5(_) | PersistedOperationPool::V12(_) => {
|
||||
return Err(OpPoolError::IncorrectOpPoolVariant)
|
||||
}
|
||||
PersistedOperationPool::V14(ref pool) => {
|
||||
let mut map = AttestationMap::default();
|
||||
for (att, attesting_indices) in pool.attestations {
|
||||
for (att, attesting_indices) in pool.attestations.clone() {
|
||||
map.insert(att, attesting_indices);
|
||||
}
|
||||
RwLock::new(map)
|
||||
}
|
||||
};
|
||||
let bls_to_execution_changes = match self {
|
||||
PersistedOperationPool::V5(_) | PersistedOperationPool::V12(_) => {
|
||||
return Err(OpPoolError::IncorrectOpPoolVariant)
|
||||
}
|
||||
PersistedOperationPool::V14(pool) => RwLock::new(
|
||||
pool.bls_to_execution_changes
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|bls_to_execution_change| {
|
||||
(
|
||||
bls_to_execution_change.as_inner().message.validator_index,
|
||||
bls_to_execution_change,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
let op_pool = OperationPool {
|
||||
attestations,
|
||||
sync_contributions,
|
||||
attester_slashings,
|
||||
proposer_slashings,
|
||||
voluntary_exits,
|
||||
// FIXME(capella): implement schema migration for address changes in op pool
|
||||
#[cfg(feature = "withdrawals-processing")]
|
||||
bls_to_execution_changes: Default::default(),
|
||||
bls_to_execution_changes,
|
||||
reward_cache: Default::default(),
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
@@ -166,6 +194,20 @@ impl<T: EthSpec> StoreItem for PersistedOperationPoolV5<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EthSpec> StoreItem for PersistedOperationPoolV12<T> {
|
||||
fn db_column() -> DBColumn {
|
||||
DBColumn::OpPool
|
||||
}
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
self.as_ssz_bytes()
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, StoreError> {
|
||||
PersistedOperationPoolV12::from_ssz_bytes(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialization for `PersistedOperationPool` defaults to `PersistedOperationPool::V12`.
|
||||
impl<T: EthSpec> StoreItem for PersistedOperationPool<T> {
|
||||
fn db_column() -> DBColumn {
|
||||
@@ -178,8 +220,8 @@ impl<T: EthSpec> StoreItem for PersistedOperationPool<T> {
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, StoreError> {
|
||||
// Default deserialization to the latest variant.
|
||||
PersistedOperationPoolV12::from_ssz_bytes(bytes)
|
||||
.map(Self::V12)
|
||||
PersistedOperationPoolV14::from_ssz_bytes(bytes)
|
||||
.map(Self::V14)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use self::UpdatePattern::*;
|
||||
use crate::*;
|
||||
use ssz::{Decode, Encode};
|
||||
use typenum::Unsigned;
|
||||
use types::historical_summary::HistoricalSummary;
|
||||
|
||||
/// Description of how a `BeaconState` field is updated during state processing.
|
||||
///
|
||||
@@ -26,7 +27,18 @@ use typenum::Unsigned;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UpdatePattern {
|
||||
/// The value is updated once per `n` slots.
|
||||
OncePerNSlots { n: u64 },
|
||||
OncePerNSlots {
|
||||
n: u64,
|
||||
/// The slot at which the field begins to accumulate values.
|
||||
///
|
||||
/// The field should not be read or written until `activation_slot` is reached, and the
|
||||
/// activation slot should act as an offset when converting slots to vector indices.
|
||||
activation_slot: Option<Slot>,
|
||||
/// The slot at which the field ceases to accumulate values.
|
||||
///
|
||||
/// If this is `None` then the field is continually updated.
|
||||
deactivation_slot: Option<Slot>,
|
||||
},
|
||||
/// The value is updated once per epoch, for the epoch `current_epoch - lag`.
|
||||
OncePerEpoch { lag: u64 },
|
||||
}
|
||||
@@ -98,12 +110,30 @@ pub trait Field<E: EthSpec>: Copy {
|
||||
fn start_and_end_vindex(current_slot: Slot, spec: &ChainSpec) -> (usize, usize) {
|
||||
// We take advantage of saturating subtraction on slots and epochs
|
||||
match Self::update_pattern(spec) {
|
||||
OncePerNSlots { n } => {
|
||||
OncePerNSlots {
|
||||
n,
|
||||
activation_slot,
|
||||
deactivation_slot,
|
||||
} => {
|
||||
// Per-slot changes exclude the index for the current slot, because
|
||||
// it won't be set until the slot completes (think of `state_roots`, `block_roots`).
|
||||
// This also works for the `historical_roots` because at the `n`th slot, the 0th
|
||||
// entry of the list is created, and before that the list is empty.
|
||||
let end_vindex = current_slot / n;
|
||||
//
|
||||
// To account for the switch from historical roots to historical summaries at
|
||||
// Capella we also modify the current slot by the activation and deactivation slots.
|
||||
// The activation slot acts as an offset (subtraction) while the deactivation slot
|
||||
// acts as a clamp (min).
|
||||
let slot_with_clamp = deactivation_slot.map_or(current_slot, |deactivation_slot| {
|
||||
std::cmp::min(current_slot, deactivation_slot)
|
||||
});
|
||||
let slot_with_clamp_and_offset = if let Some(activation_slot) = activation_slot {
|
||||
slot_with_clamp - activation_slot
|
||||
} else {
|
||||
// Return (0, 0) to indicate that the field should not be read/written.
|
||||
return (0, 0);
|
||||
};
|
||||
let end_vindex = slot_with_clamp_and_offset / n;
|
||||
let start_vindex = end_vindex - Self::Length::to_u64();
|
||||
(start_vindex.as_usize(), end_vindex.as_usize())
|
||||
}
|
||||
@@ -295,7 +325,11 @@ field!(
|
||||
Hash256,
|
||||
T::SlotsPerHistoricalRoot,
|
||||
DBColumn::BeaconBlockRoots,
|
||||
|_| OncePerNSlots { n: 1 },
|
||||
|_| OncePerNSlots {
|
||||
n: 1,
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.block_roots(), index)
|
||||
);
|
||||
|
||||
@@ -305,7 +339,11 @@ field!(
|
||||
Hash256,
|
||||
T::SlotsPerHistoricalRoot,
|
||||
DBColumn::BeaconStateRoots,
|
||||
|_| OncePerNSlots { n: 1 },
|
||||
|_| OncePerNSlots {
|
||||
n: 1,
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.state_roots(), index)
|
||||
);
|
||||
|
||||
@@ -315,8 +353,12 @@ field!(
|
||||
Hash256,
|
||||
T::HistoricalRootsLimit,
|
||||
DBColumn::BeaconHistoricalRoots,
|
||||
|_| OncePerNSlots {
|
||||
n: T::SlotsPerHistoricalRoot::to_u64()
|
||||
|spec: &ChainSpec| OncePerNSlots {
|
||||
n: T::SlotsPerHistoricalRoot::to_u64(),
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: spec
|
||||
.capella_fork_epoch
|
||||
.map(|fork_epoch| fork_epoch.start_slot(T::slots_per_epoch())),
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.historical_roots(), index)
|
||||
);
|
||||
@@ -331,6 +373,27 @@ field!(
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.randao_mixes(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
HistoricalSummaries,
|
||||
VariableLengthField,
|
||||
HistoricalSummary,
|
||||
T::HistoricalRootsLimit,
|
||||
DBColumn::BeaconHistoricalSummaries,
|
||||
|spec: &ChainSpec| OncePerNSlots {
|
||||
n: T::SlotsPerHistoricalRoot::to_u64(),
|
||||
activation_slot: spec
|
||||
.capella_fork_epoch
|
||||
.map(|fork_epoch| fork_epoch.start_slot(T::slots_per_epoch())),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(
|
||||
state
|
||||
.historical_summaries()
|
||||
.map_err(|_| ChunkError::InvalidFork)?,
|
||||
index
|
||||
)
|
||||
);
|
||||
|
||||
pub fn store_updated_vector<F: Field<E>, E: EthSpec, S: KeyValueStore<E>>(
|
||||
field: F,
|
||||
store: &S,
|
||||
@@ -679,6 +742,7 @@ pub enum ChunkError {
|
||||
end_vindex: usize,
|
||||
length: usize,
|
||||
},
|
||||
InvalidFork,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -801,7 +865,7 @@ mod test {
|
||||
|
||||
fn needs_genesis_value_test_randao<F: Field<TestSpec>>(_: F) {
|
||||
let spec = &TestSpec::default_spec();
|
||||
let max = TestSpec::slots_per_epoch() as u64 * (F::Length::to_u64() - 1);
|
||||
let max = TestSpec::slots_per_epoch() * (F::Length::to_u64() - 1);
|
||||
for i in 0..max {
|
||||
assert!(
|
||||
F::slot_needs_genesis_value(Slot::new(i), spec),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::chunked_vector::{
|
||||
store_updated_vector, BlockRoots, HistoricalRoots, RandaoMixes, StateRoots,
|
||||
store_updated_vector, BlockRoots, HistoricalRoots, HistoricalSummaries, RandaoMixes, StateRoots,
|
||||
};
|
||||
use crate::config::{
|
||||
OnDiskStoreConfig, StoreConfig, DEFAULT_SLOTS_PER_RESTORE_POINT,
|
||||
@@ -952,6 +952,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
store_updated_vector(StateRoots, db, state, &self.spec, ops)?;
|
||||
store_updated_vector(HistoricalRoots, db, state, &self.spec, ops)?;
|
||||
store_updated_vector(RandaoMixes, db, state, &self.spec, ops)?;
|
||||
store_updated_vector(HistoricalSummaries, db, state, &self.spec, ops)?;
|
||||
|
||||
// 3. Store restore point.
|
||||
let restore_point_index = state.slot().as_u64() / self.config.slots_per_restore_point;
|
||||
@@ -1006,6 +1007,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
partial_state.load_state_roots(&self.cold_db, &self.spec)?;
|
||||
partial_state.load_historical_roots(&self.cold_db, &self.spec)?;
|
||||
partial_state.load_randao_mixes(&self.cold_db, &self.spec)?;
|
||||
partial_state.load_historical_summaries(&self.cold_db, &self.spec)?;
|
||||
|
||||
partial_state.try_into()
|
||||
}
|
||||
|
||||
@@ -215,6 +215,8 @@ pub enum DBColumn {
|
||||
/// For Optimistically Imported Merge Transition Blocks
|
||||
#[strum(serialize = "otb")]
|
||||
OptimisticTransitionBlock,
|
||||
#[strum(serialize = "bhs")]
|
||||
BeaconHistoricalSummaries,
|
||||
}
|
||||
|
||||
/// A block from the database, which might have an execution payload or not.
|
||||
|
||||
@@ -4,7 +4,7 @@ use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use types::{Checkpoint, Hash256, Slot};
|
||||
|
||||
pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(13);
|
||||
pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(14);
|
||||
|
||||
// All the keys that get stored under the `BeaconMeta` column.
|
||||
//
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::chunked_vector::{
|
||||
load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots, RandaoMixes,
|
||||
StateRoots,
|
||||
load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots,
|
||||
HistoricalSummaries, RandaoMixes, StateRoots,
|
||||
};
|
||||
use crate::{get_key_for_col, DBColumn, Error, KeyValueStore, KeyValueStoreOp};
|
||||
use ssz::{Decode, DecodeError, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use types::historical_summary::HistoricalSummary;
|
||||
use types::superstruct;
|
||||
use types::*;
|
||||
|
||||
@@ -104,16 +105,20 @@ where
|
||||
)]
|
||||
pub latest_execution_payload_header: ExecutionPayloadHeaderEip4844<T>,
|
||||
|
||||
// Withdrawals
|
||||
// Capella
|
||||
#[superstruct(only(Capella, Eip4844))]
|
||||
pub next_withdrawal_index: u64,
|
||||
#[superstruct(only(Capella, Eip4844))]
|
||||
pub next_withdrawal_validator_index: u64,
|
||||
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
#[superstruct(only(Capella, Eip4844))]
|
||||
pub historical_summaries: Option<VariableList<HistoricalSummary, T::HistoricalRootsLimit>>,
|
||||
}
|
||||
|
||||
/// Implement the conversion function from BeaconState -> PartialBeaconState.
|
||||
macro_rules! impl_from_state_forgetful {
|
||||
($s:ident, $outer:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*]) => {
|
||||
($s:ident, $outer:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*], [$($extra_fields_opt:ident),*]) => {
|
||||
PartialBeaconState::$variant_name($struct_name {
|
||||
// Versioning
|
||||
genesis_time: $s.genesis_time,
|
||||
@@ -154,6 +159,11 @@ macro_rules! impl_from_state_forgetful {
|
||||
// Variant-specific fields
|
||||
$(
|
||||
$extra_fields: $s.$extra_fields.clone()
|
||||
),*,
|
||||
|
||||
// Variant-specific optional
|
||||
$(
|
||||
$extra_fields_opt: None
|
||||
),*
|
||||
})
|
||||
}
|
||||
@@ -168,7 +178,8 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
outer,
|
||||
Base,
|
||||
PartialBeaconStateBase,
|
||||
[previous_epoch_attestations, current_epoch_attestations]
|
||||
[previous_epoch_attestations, current_epoch_attestations],
|
||||
[]
|
||||
),
|
||||
BeaconState::Altair(s) => impl_from_state_forgetful!(
|
||||
s,
|
||||
@@ -181,7 +192,8 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
current_sync_committee,
|
||||
next_sync_committee,
|
||||
inactivity_scores
|
||||
]
|
||||
],
|
||||
[]
|
||||
),
|
||||
BeaconState::Merge(s) => impl_from_state_forgetful!(
|
||||
s,
|
||||
@@ -195,7 +207,8 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
next_sync_committee,
|
||||
inactivity_scores,
|
||||
latest_execution_payload_header
|
||||
]
|
||||
],
|
||||
[]
|
||||
),
|
||||
BeaconState::Capella(s) => impl_from_state_forgetful!(
|
||||
s,
|
||||
@@ -211,7 +224,8 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
latest_execution_payload_header,
|
||||
next_withdrawal_index,
|
||||
next_withdrawal_validator_index
|
||||
]
|
||||
],
|
||||
[historical_summaries]
|
||||
),
|
||||
BeaconState::Eip4844(s) => impl_from_state_forgetful!(
|
||||
s,
|
||||
@@ -227,7 +241,8 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
latest_execution_payload_header,
|
||||
next_withdrawal_index,
|
||||
next_withdrawal_validator_index
|
||||
]
|
||||
],
|
||||
[historical_summaries]
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -303,6 +318,23 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_historical_summaries<S: KeyValueStore<T>>(
|
||||
&mut self,
|
||||
store: &S,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
let slot = self.slot();
|
||||
if let Ok(historical_summaries) = self.historical_summaries_mut() {
|
||||
if historical_summaries.is_none() {
|
||||
*historical_summaries =
|
||||
Some(load_variable_list_from_db::<HistoricalSummaries, T, _>(
|
||||
store, slot, spec,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_randao_mixes<S: KeyValueStore<T>>(
|
||||
&mut self,
|
||||
store: &S,
|
||||
@@ -326,7 +358,7 @@ impl<T: EthSpec> PartialBeaconState<T> {
|
||||
|
||||
/// Implement the conversion from PartialBeaconState -> BeaconState.
|
||||
macro_rules! impl_try_into_beacon_state {
|
||||
($inner:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*]) => {
|
||||
($inner:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*], [$($extra_opt_fields:ident),*]) => {
|
||||
BeaconState::$variant_name($struct_name {
|
||||
// Versioning
|
||||
genesis_time: $inner.genesis_time,
|
||||
@@ -371,6 +403,11 @@ macro_rules! impl_try_into_beacon_state {
|
||||
// Variant-specific fields
|
||||
$(
|
||||
$extra_fields: $inner.$extra_fields
|
||||
),*,
|
||||
|
||||
// Variant-specific optional fields
|
||||
$(
|
||||
$extra_opt_fields: unpack_field($inner.$extra_opt_fields)?
|
||||
),*
|
||||
})
|
||||
}
|
||||
@@ -389,7 +426,8 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
|
||||
inner,
|
||||
Base,
|
||||
BeaconStateBase,
|
||||
[previous_epoch_attestations, current_epoch_attestations]
|
||||
[previous_epoch_attestations, current_epoch_attestations],
|
||||
[]
|
||||
),
|
||||
PartialBeaconState::Altair(inner) => impl_try_into_beacon_state!(
|
||||
inner,
|
||||
@@ -401,7 +439,8 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
|
||||
current_sync_committee,
|
||||
next_sync_committee,
|
||||
inactivity_scores
|
||||
]
|
||||
],
|
||||
[]
|
||||
),
|
||||
PartialBeaconState::Merge(inner) => impl_try_into_beacon_state!(
|
||||
inner,
|
||||
@@ -414,7 +453,8 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
|
||||
next_sync_committee,
|
||||
inactivity_scores,
|
||||
latest_execution_payload_header
|
||||
]
|
||||
],
|
||||
[]
|
||||
),
|
||||
PartialBeaconState::Capella(inner) => impl_try_into_beacon_state!(
|
||||
inner,
|
||||
@@ -429,7 +469,8 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
|
||||
latest_execution_payload_header,
|
||||
next_withdrawal_index,
|
||||
next_withdrawal_validator_index
|
||||
]
|
||||
],
|
||||
[historical_summaries]
|
||||
),
|
||||
PartialBeaconState::Eip4844(inner) => impl_try_into_beacon_state!(
|
||||
inner,
|
||||
@@ -444,7 +485,8 @@ impl<E: EthSpec> TryInto<BeaconState<E>> for PartialBeaconState<E> {
|
||||
latest_execution_payload_header,
|
||||
next_withdrawal_index,
|
||||
next_withdrawal_validator_index
|
||||
]
|
||||
],
|
||||
[historical_summaries]
|
||||
),
|
||||
};
|
||||
Ok(state)
|
||||
|
||||
Reference in New Issue
Block a user