mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-19 21:04:41 +00:00
Realized unrealized experimentation (#3322)
## Issue Addressed Add a flag that optionally enables unrealized vote tracking. Would like to test out on testnets and benchmark differences in methods of vote tracking. This PR includes a DB schema upgrade to enable to new vote tracking style. Co-authored-by: realbigsean <sean@sigmaprime.io> Co-authored-by: Paul Hauner <paul@paulhauner.com> Co-authored-by: sean <seananderson33@gmail.com> Co-authored-by: Mac L <mjladson@pm.me>
This commit is contained in:
@@ -78,7 +78,7 @@ impl ForkChoiceTestDefinition {
|
||||
|
||||
let junk_shuffling_id =
|
||||
AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero());
|
||||
let mut fork_choice = ProtoArrayForkChoice::new(
|
||||
let mut fork_choice = ProtoArrayForkChoice::new::<MainnetEthSpec>(
|
||||
self.finalized_block_slot,
|
||||
Hash256::zero(),
|
||||
self.justified_checkpoint,
|
||||
@@ -103,6 +103,7 @@ impl ForkChoiceTestDefinition {
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
Hash256::zero(),
|
||||
Slot::new(0),
|
||||
&spec,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -129,6 +130,7 @@ impl ForkChoiceTestDefinition {
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
proposer_boost_root,
|
||||
Slot::new(0),
|
||||
&spec,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -152,6 +154,7 @@ impl ForkChoiceTestDefinition {
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
Hash256::zero(),
|
||||
Slot::new(0),
|
||||
&spec,
|
||||
);
|
||||
|
||||
@@ -190,13 +193,17 @@ impl ForkChoiceTestDefinition {
|
||||
execution_status: ExecutionStatus::Optimistic(
|
||||
ExecutionBlockHash::from_root(root),
|
||||
),
|
||||
unrealized_justified_checkpoint: None,
|
||||
unrealized_finalized_checkpoint: None,
|
||||
};
|
||||
fork_choice.process_block(block).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"process_block op at index {} returned error: {:?}",
|
||||
op_index, e
|
||||
)
|
||||
});
|
||||
fork_choice
|
||||
.process_block::<MainnetEthSpec>(block, slot)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"process_block op at index {} returned error: {:?}",
|
||||
op_index, e
|
||||
)
|
||||
});
|
||||
check_bytes_round_trip(&fork_choice);
|
||||
}
|
||||
Operation::ProcessAttestation {
|
||||
|
||||
@@ -97,6 +97,10 @@ pub struct ProtoNode {
|
||||
/// Indicates if an execution node has marked this block as valid. Also contains the execution
|
||||
/// block hash.
|
||||
pub execution_status: ExecutionStatus,
|
||||
#[ssz(with = "four_byte_option_checkpoint")]
|
||||
pub unrealized_justified_checkpoint: Option<Checkpoint>,
|
||||
#[ssz(with = "four_byte_option_checkpoint")]
|
||||
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Copy, Clone)]
|
||||
@@ -140,6 +144,7 @@ impl ProtoArray {
|
||||
/// - Compare the current node with the parents best-child, updating it if the current node
|
||||
/// should become the best child.
|
||||
/// - If required, update the parents best-descendant with the current node or its best-descendant.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn apply_score_changes<E: EthSpec>(
|
||||
&mut self,
|
||||
mut deltas: Vec<i64>,
|
||||
@@ -147,6 +152,7 @@ impl ProtoArray {
|
||||
finalized_checkpoint: Checkpoint,
|
||||
new_balances: &[u64],
|
||||
proposer_boost_root: Hash256,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
if deltas.len() != self.indices.len() {
|
||||
@@ -280,7 +286,11 @@ impl ProtoArray {
|
||||
|
||||
// If the node has a parent, try to update its best-child and best-descendant.
|
||||
if let Some(parent_index) = node.parent {
|
||||
self.maybe_update_best_child_and_descendant(parent_index, node_index)?;
|
||||
self.maybe_update_best_child_and_descendant::<E>(
|
||||
parent_index,
|
||||
node_index,
|
||||
current_slot,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +300,7 @@ impl ProtoArray {
|
||||
/// Register a block with the fork choice.
|
||||
///
|
||||
/// It is only sane to supply a `None` parent for the genesis block.
|
||||
pub fn on_block(&mut self, block: Block) -> Result<(), Error> {
|
||||
pub fn on_block<E: EthSpec>(&mut self, block: Block, current_slot: Slot) -> Result<(), Error> {
|
||||
// If the block is already known, simply ignore it.
|
||||
if self.indices.contains_key(&block.root) {
|
||||
return Ok(());
|
||||
@@ -314,6 +324,8 @@ impl ProtoArray {
|
||||
best_child: None,
|
||||
best_descendant: None,
|
||||
execution_status: block.execution_status,
|
||||
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint,
|
||||
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
|
||||
};
|
||||
|
||||
// If the parent has an invalid execution status, return an error before adding the block to
|
||||
@@ -335,7 +347,11 @@ impl ProtoArray {
|
||||
self.nodes.push(node.clone());
|
||||
|
||||
if let Some(parent_index) = node.parent {
|
||||
self.maybe_update_best_child_and_descendant(parent_index, node_index)?;
|
||||
self.maybe_update_best_child_and_descendant::<E>(
|
||||
parent_index,
|
||||
node_index,
|
||||
current_slot,
|
||||
)?;
|
||||
|
||||
if matches!(block.execution_status, ExecutionStatus::Valid(_)) {
|
||||
self.propagate_execution_payload_validation_by_index(parent_index)?;
|
||||
@@ -604,7 +620,11 @@ impl ProtoArray {
|
||||
/// been called without a subsequent `Self::apply_score_changes` call. This is because
|
||||
/// `on_new_block` does not attempt to walk backwards through the tree and update the
|
||||
/// best-child/best-descendant links.
|
||||
pub fn find_head(&self, justified_root: &Hash256) -> Result<Hash256, Error> {
|
||||
pub fn find_head<E: EthSpec>(
|
||||
&self,
|
||||
justified_root: &Hash256,
|
||||
current_slot: Slot,
|
||||
) -> Result<Hash256, Error> {
|
||||
let justified_index = self
|
||||
.indices
|
||||
.get(justified_root)
|
||||
@@ -637,7 +657,7 @@ impl ProtoArray {
|
||||
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
|
||||
|
||||
// Perform a sanity check that the node is indeed valid to be the head.
|
||||
if !self.node_is_viable_for_head(best_node) {
|
||||
if !self.node_is_viable_for_head::<E>(best_node, current_slot) {
|
||||
return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo {
|
||||
start_root: *justified_root,
|
||||
justified_checkpoint: self.justified_checkpoint,
|
||||
@@ -733,10 +753,11 @@ impl ProtoArray {
|
||||
/// best-descendant.
|
||||
/// - The child is not the best child but becomes the best child.
|
||||
/// - The child is not the best child and does not become the best child.
|
||||
fn maybe_update_best_child_and_descendant(
|
||||
fn maybe_update_best_child_and_descendant<E: EthSpec>(
|
||||
&mut self,
|
||||
parent_index: usize,
|
||||
child_index: usize,
|
||||
current_slot: Slot,
|
||||
) -> Result<(), Error> {
|
||||
let child = self
|
||||
.nodes
|
||||
@@ -748,7 +769,8 @@ impl ProtoArray {
|
||||
.get(parent_index)
|
||||
.ok_or(Error::InvalidNodeIndex(parent_index))?;
|
||||
|
||||
let child_leads_to_viable_head = self.node_leads_to_viable_head(child)?;
|
||||
let child_leads_to_viable_head =
|
||||
self.node_leads_to_viable_head::<E>(child, current_slot)?;
|
||||
|
||||
// These three variables are aliases to the three options that we may set the
|
||||
// `parent.best_child` and `parent.best_descendant` to.
|
||||
@@ -761,54 +783,54 @@ impl ProtoArray {
|
||||
);
|
||||
let no_change = (parent.best_child, parent.best_descendant);
|
||||
|
||||
let (new_best_child, new_best_descendant) = if let Some(best_child_index) =
|
||||
parent.best_child
|
||||
{
|
||||
if best_child_index == child_index && !child_leads_to_viable_head {
|
||||
// If the child is already the best-child of the parent but it's not viable for
|
||||
// the head, remove it.
|
||||
change_to_none
|
||||
} else if best_child_index == child_index {
|
||||
// If the child is the best-child already, set it again to ensure that the
|
||||
// best-descendant of the parent is updated.
|
||||
change_to_child
|
||||
} else {
|
||||
let best_child = self
|
||||
.nodes
|
||||
.get(best_child_index)
|
||||
.ok_or(Error::InvalidBestDescendant(best_child_index))?;
|
||||
|
||||
let best_child_leads_to_viable_head = self.node_leads_to_viable_head(best_child)?;
|
||||
|
||||
if child_leads_to_viable_head && !best_child_leads_to_viable_head {
|
||||
// The child leads to a viable head, but the current best-child doesn't.
|
||||
let (new_best_child, new_best_descendant) =
|
||||
if let Some(best_child_index) = parent.best_child {
|
||||
if best_child_index == child_index && !child_leads_to_viable_head {
|
||||
// If the child is already the best-child of the parent but it's not viable for
|
||||
// the head, remove it.
|
||||
change_to_none
|
||||
} else if best_child_index == child_index {
|
||||
// If the child is the best-child already, set it again to ensure that the
|
||||
// best-descendant of the parent is updated.
|
||||
change_to_child
|
||||
} else if !child_leads_to_viable_head && best_child_leads_to_viable_head {
|
||||
// The best child leads to a viable head, but the child doesn't.
|
||||
no_change
|
||||
} else if child.weight == best_child.weight {
|
||||
// Tie-breaker of equal weights by root.
|
||||
if child.root >= best_child.root {
|
||||
change_to_child
|
||||
} else {
|
||||
no_change
|
||||
}
|
||||
} else {
|
||||
// Choose the winner by weight.
|
||||
if child.weight >= best_child.weight {
|
||||
let best_child = self
|
||||
.nodes
|
||||
.get(best_child_index)
|
||||
.ok_or(Error::InvalidBestDescendant(best_child_index))?;
|
||||
|
||||
let best_child_leads_to_viable_head =
|
||||
self.node_leads_to_viable_head::<E>(best_child, current_slot)?;
|
||||
|
||||
if child_leads_to_viable_head && !best_child_leads_to_viable_head {
|
||||
// The child leads to a viable head, but the current best-child doesn't.
|
||||
change_to_child
|
||||
} else {
|
||||
} else if !child_leads_to_viable_head && best_child_leads_to_viable_head {
|
||||
// The best child leads to a viable head, but the child doesn't.
|
||||
no_change
|
||||
} else if child.weight == best_child.weight {
|
||||
// Tie-breaker of equal weights by root.
|
||||
if child.root >= best_child.root {
|
||||
change_to_child
|
||||
} else {
|
||||
no_change
|
||||
}
|
||||
} else {
|
||||
// Choose the winner by weight.
|
||||
if child.weight >= best_child.weight {
|
||||
change_to_child
|
||||
} else {
|
||||
no_change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if child_leads_to_viable_head {
|
||||
// There is no current best-child and the child is viable.
|
||||
change_to_child
|
||||
} else {
|
||||
// There is no current best-child but the child is not viable.
|
||||
no_change
|
||||
};
|
||||
} else if child_leads_to_viable_head {
|
||||
// There is no current best-child and the child is viable.
|
||||
change_to_child
|
||||
} else {
|
||||
// There is no current best-child but the child is not viable.
|
||||
no_change
|
||||
};
|
||||
|
||||
let parent = self
|
||||
.nodes
|
||||
@@ -823,7 +845,11 @@ impl ProtoArray {
|
||||
|
||||
/// Indicates if the node itself is viable for the head, or if it's best descendant is viable
|
||||
/// for the head.
|
||||
fn node_leads_to_viable_head(&self, node: &ProtoNode) -> Result<bool, Error> {
|
||||
fn node_leads_to_viable_head<E: EthSpec>(
|
||||
&self,
|
||||
node: &ProtoNode,
|
||||
current_slot: Slot,
|
||||
) -> Result<bool, Error> {
|
||||
let best_descendant_is_viable_for_head =
|
||||
if let Some(best_descendant_index) = node.best_descendant {
|
||||
let best_descendant = self
|
||||
@@ -831,12 +857,13 @@ impl ProtoArray {
|
||||
.get(best_descendant_index)
|
||||
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
|
||||
|
||||
self.node_is_viable_for_head(best_descendant)
|
||||
self.node_is_viable_for_head::<E>(best_descendant, current_slot)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(best_descendant_is_viable_for_head || self.node_is_viable_for_head(node))
|
||||
Ok(best_descendant_is_viable_for_head
|
||||
|| self.node_is_viable_for_head::<E>(node, current_slot))
|
||||
}
|
||||
|
||||
/// This is the equivalent to the `filter_block_tree` function in the eth2 spec:
|
||||
@@ -845,18 +872,43 @@ impl ProtoArray {
|
||||
///
|
||||
/// Any node that has a different finalized or justified epoch should not be viable for the
|
||||
/// head.
|
||||
fn node_is_viable_for_head(&self, node: &ProtoNode) -> bool {
|
||||
fn node_is_viable_for_head<E: EthSpec>(&self, node: &ProtoNode, current_slot: Slot) -> bool {
|
||||
if node.execution_status.is_invalid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let (Some(node_justified_checkpoint), Some(node_finalized_checkpoint)) =
|
||||
let checkpoint_match_predicate =
|
||||
|node_justified_checkpoint: Checkpoint, node_finalized_checkpoint: Checkpoint| {
|
||||
let correct_justified = node_justified_checkpoint == self.justified_checkpoint
|
||||
|| self.justified_checkpoint.epoch == Epoch::new(0);
|
||||
let correct_finalized = node_finalized_checkpoint == self.finalized_checkpoint
|
||||
|| self.finalized_checkpoint.epoch == Epoch::new(0);
|
||||
correct_justified && correct_finalized
|
||||
};
|
||||
|
||||
if let (
|
||||
Some(unrealized_justified_checkpoint),
|
||||
Some(unrealized_finalized_checkpoint),
|
||||
Some(justified_checkpoint),
|
||||
Some(finalized_checkpoint),
|
||||
) = (
|
||||
node.unrealized_justified_checkpoint,
|
||||
node.unrealized_finalized_checkpoint,
|
||||
node.justified_checkpoint,
|
||||
node.finalized_checkpoint,
|
||||
) {
|
||||
if node.slot.epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) {
|
||||
checkpoint_match_predicate(
|
||||
unrealized_justified_checkpoint,
|
||||
unrealized_finalized_checkpoint,
|
||||
)
|
||||
} else {
|
||||
checkpoint_match_predicate(justified_checkpoint, finalized_checkpoint)
|
||||
}
|
||||
} else if let (Some(justified_checkpoint), Some(finalized_checkpoint)) =
|
||||
(node.justified_checkpoint, node.finalized_checkpoint)
|
||||
{
|
||||
(node_justified_checkpoint == self.justified_checkpoint
|
||||
|| self.justified_checkpoint.epoch == Epoch::new(0))
|
||||
&& (node_finalized_checkpoint == self.finalized_checkpoint
|
||||
|| self.finalized_checkpoint.epoch == Epoch::new(0))
|
||||
checkpoint_match_predicate(justified_checkpoint, finalized_checkpoint)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -124,6 +124,8 @@ pub struct Block {
|
||||
/// Indicates if an execution node has marked this block as valid. Also contains the execution
|
||||
/// block hash.
|
||||
pub execution_status: ExecutionStatus,
|
||||
pub unrealized_justified_checkpoint: Option<Checkpoint>,
|
||||
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
|
||||
}
|
||||
|
||||
/// A Vec-wrapper which will grow to match any request.
|
||||
@@ -162,7 +164,7 @@ pub struct ProtoArrayForkChoice {
|
||||
|
||||
impl ProtoArrayForkChoice {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
pub fn new<E: EthSpec>(
|
||||
finalized_block_slot: Slot,
|
||||
finalized_block_state_root: Hash256,
|
||||
justified_checkpoint: Checkpoint,
|
||||
@@ -193,10 +195,12 @@ impl ProtoArrayForkChoice {
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
execution_status,
|
||||
unrealized_justified_checkpoint: Some(justified_checkpoint),
|
||||
unrealized_finalized_checkpoint: Some(finalized_checkpoint),
|
||||
};
|
||||
|
||||
proto_array
|
||||
.on_block(block)
|
||||
.on_block::<E>(block, finalized_block_slot)
|
||||
.map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -242,13 +246,17 @@ impl ProtoArrayForkChoice {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_block(&mut self, block: Block) -> Result<(), String> {
|
||||
pub fn process_block<E: EthSpec>(
|
||||
&mut self,
|
||||
block: Block,
|
||||
current_slot: Slot,
|
||||
) -> Result<(), String> {
|
||||
if block.parent_root.is_none() {
|
||||
return Err("Missing parent root".to_string());
|
||||
}
|
||||
|
||||
self.proto_array
|
||||
.on_block(block)
|
||||
.on_block::<E>(block, current_slot)
|
||||
.map_err(|e| format!("process_block_error: {:?}", e))
|
||||
}
|
||||
|
||||
@@ -258,6 +266,7 @@ impl ProtoArrayForkChoice {
|
||||
finalized_checkpoint: Checkpoint,
|
||||
justified_state_balances: &[u64],
|
||||
proposer_boost_root: Hash256,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Hash256, String> {
|
||||
let old_balances = &mut self.balances;
|
||||
@@ -279,6 +288,7 @@ impl ProtoArrayForkChoice {
|
||||
finalized_checkpoint,
|
||||
new_balances,
|
||||
proposer_boost_root,
|
||||
current_slot,
|
||||
spec,
|
||||
)
|
||||
.map_err(|e| format!("find_head apply_score_changes failed: {:?}", e))?;
|
||||
@@ -286,7 +296,7 @@ impl ProtoArrayForkChoice {
|
||||
*old_balances = new_balances.to_vec();
|
||||
|
||||
self.proto_array
|
||||
.find_head(&justified_checkpoint.root)
|
||||
.find_head::<E>(&justified_checkpoint.root, current_slot)
|
||||
.map_err(|e| format!("find_head failed: {:?}", e))
|
||||
}
|
||||
|
||||
@@ -341,6 +351,8 @@ impl ProtoArrayForkChoice {
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
execution_status: block.execution_status,
|
||||
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint,
|
||||
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -485,6 +497,7 @@ fn compute_deltas(
|
||||
#[cfg(test)]
|
||||
mod test_compute_deltas {
|
||||
use super::*;
|
||||
use types::MainnetEthSpec;
|
||||
|
||||
/// Gives a hash that is not the zero hash (unless i is `usize::max_value)`.
|
||||
fn hash_from_index(i: usize) -> Hash256 {
|
||||
@@ -510,7 +523,7 @@ mod test_compute_deltas {
|
||||
root: finalized_root,
|
||||
};
|
||||
|
||||
let mut fc = ProtoArrayForkChoice::new(
|
||||
let mut fc = ProtoArrayForkChoice::new::<MainnetEthSpec>(
|
||||
genesis_slot,
|
||||
state_root,
|
||||
genesis_checkpoint,
|
||||
@@ -523,34 +536,44 @@ mod test_compute_deltas {
|
||||
|
||||
// Add block that is a finalized descendant.
|
||||
fc.proto_array
|
||||
.on_block(Block {
|
||||
slot: genesis_slot + 1,
|
||||
root: finalized_desc,
|
||||
parent_root: Some(finalized_root),
|
||||
state_root,
|
||||
target_root: finalized_root,
|
||||
current_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
next_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
justified_checkpoint: genesis_checkpoint,
|
||||
finalized_checkpoint: genesis_checkpoint,
|
||||
execution_status,
|
||||
})
|
||||
.on_block::<MainnetEthSpec>(
|
||||
Block {
|
||||
slot: genesis_slot + 1,
|
||||
root: finalized_desc,
|
||||
parent_root: Some(finalized_root),
|
||||
state_root,
|
||||
target_root: finalized_root,
|
||||
current_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
next_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
justified_checkpoint: genesis_checkpoint,
|
||||
finalized_checkpoint: genesis_checkpoint,
|
||||
execution_status,
|
||||
unrealized_justified_checkpoint: Some(genesis_checkpoint),
|
||||
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
|
||||
},
|
||||
genesis_slot + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Add block that is *not* a finalized descendant.
|
||||
fc.proto_array
|
||||
.on_block(Block {
|
||||
slot: genesis_slot + 1,
|
||||
root: not_finalized_desc,
|
||||
parent_root: None,
|
||||
state_root,
|
||||
target_root: finalized_root,
|
||||
current_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
next_epoch_shuffling_id: junk_shuffling_id,
|
||||
justified_checkpoint: genesis_checkpoint,
|
||||
finalized_checkpoint: genesis_checkpoint,
|
||||
execution_status,
|
||||
})
|
||||
.on_block::<MainnetEthSpec>(
|
||||
Block {
|
||||
slot: genesis_slot + 1,
|
||||
root: not_finalized_desc,
|
||||
parent_root: None,
|
||||
state_root,
|
||||
target_root: finalized_root,
|
||||
current_epoch_shuffling_id: junk_shuffling_id.clone(),
|
||||
next_epoch_shuffling_id: junk_shuffling_id,
|
||||
justified_checkpoint: genesis_checkpoint,
|
||||
finalized_checkpoint: genesis_checkpoint,
|
||||
execution_status,
|
||||
unrealized_justified_checkpoint: None,
|
||||
unrealized_finalized_checkpoint: None,
|
||||
},
|
||||
genesis_slot + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!fc.is_descendant(unknown, unknown));
|
||||
|
||||
Reference in New Issue
Block a user