Merge remote-tracking branch 'origin/release-v7.0.0-beta.0' into unstable

This commit is contained in:
Michael Sproul
2025-02-14 10:26:36 +11:00
9 changed files with 135 additions and 98 deletions

8
Cargo.lock generated
View File

@@ -860,7 +860,7 @@ dependencies = [
[[package]] [[package]]
name = "beacon_node" name = "beacon_node"
version = "6.0.1" version = "7.0.0-beta.0"
dependencies = [ dependencies = [
"account_utils", "account_utils",
"beacon_chain", "beacon_chain",
@@ -1108,7 +1108,7 @@ dependencies = [
[[package]] [[package]]
name = "boot_node" name = "boot_node"
version = "6.0.1" version = "7.0.0-beta.0"
dependencies = [ dependencies = [
"beacon_node", "beacon_node",
"bytes", "bytes",
@@ -4811,7 +4811,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "lcli" name = "lcli"
version = "6.0.1" version = "7.0.0-beta.0"
dependencies = [ dependencies = [
"account_utils", "account_utils",
"beacon_chain", "beacon_chain",
@@ -5366,7 +5366,7 @@ dependencies = [
[[package]] [[package]]
name = "lighthouse" name = "lighthouse"
version = "6.0.1" version = "7.0.0-beta.0"
dependencies = [ dependencies = [
"account_manager", "account_manager",
"account_utils", "account_utils",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "beacon_node" name = "beacon_node"
version = "6.0.1" version = "7.0.0-beta.0"
authors = [ authors = [
"Paul Hauner <paul@paulhauner.com>", "Paul Hauner <paul@paulhauner.com>",
"Age Manning <Age@AgeManning.com", "Age Manning <Age@AgeManning.com",

View File

@@ -109,8 +109,6 @@ pub struct BeaconProcessorQueueLengths {
gossip_voluntary_exit_queue: usize, gossip_voluntary_exit_queue: usize,
gossip_proposer_slashing_queue: usize, gossip_proposer_slashing_queue: usize,
gossip_attester_slashing_queue: usize, gossip_attester_slashing_queue: usize,
finality_update_queue: usize,
optimistic_update_queue: usize,
unknown_light_client_update_queue: usize, unknown_light_client_update_queue: usize,
unknown_block_sampling_request_queue: usize, unknown_block_sampling_request_queue: usize,
rpc_block_queue: usize, rpc_block_queue: usize,
@@ -132,9 +130,11 @@ pub struct BeaconProcessorQueueLengths {
dcbroots_queue: usize, dcbroots_queue: usize,
dcbrange_queue: usize, dcbrange_queue: usize,
gossip_bls_to_execution_change_queue: usize, gossip_bls_to_execution_change_queue: usize,
lc_gossip_finality_update_queue: usize,
lc_gossip_optimistic_update_queue: usize,
lc_bootstrap_queue: usize, lc_bootstrap_queue: usize,
lc_optimistic_update_queue: usize, lc_rpc_optimistic_update_queue: usize,
lc_finality_update_queue: usize, lc_rpc_finality_update_queue: usize,
lc_update_range_queue: usize, lc_update_range_queue: usize,
api_request_p0_queue: usize, api_request_p0_queue: usize,
api_request_p1_queue: usize, api_request_p1_queue: usize,
@@ -175,15 +175,13 @@ impl BeaconProcessorQueueLengths {
gossip_voluntary_exit_queue: 4096, gossip_voluntary_exit_queue: 4096,
gossip_proposer_slashing_queue: 4096, gossip_proposer_slashing_queue: 4096,
gossip_attester_slashing_queue: 4096, gossip_attester_slashing_queue: 4096,
finality_update_queue: 1024,
optimistic_update_queue: 1024,
unknown_block_sampling_request_queue: 16384,
unknown_light_client_update_queue: 128, unknown_light_client_update_queue: 128,
rpc_block_queue: 1024, rpc_block_queue: 1024,
rpc_blob_queue: 1024, rpc_blob_queue: 1024,
// TODO(das): Placeholder values // TODO(das): Placeholder values
rpc_custody_column_queue: 1000, rpc_custody_column_queue: 1000,
rpc_verify_data_column_queue: 1000, rpc_verify_data_column_queue: 1000,
unknown_block_sampling_request_queue: 16384,
sampling_result_queue: 1000, sampling_result_queue: 1000,
chain_segment_queue: 64, chain_segment_queue: 64,
backfill_chain_segment: 64, backfill_chain_segment: 64,
@@ -200,9 +198,11 @@ impl BeaconProcessorQueueLengths {
dcbroots_queue: 1024, dcbroots_queue: 1024,
dcbrange_queue: 1024, dcbrange_queue: 1024,
gossip_bls_to_execution_change_queue: 16384, gossip_bls_to_execution_change_queue: 16384,
lc_gossip_finality_update_queue: 1024,
lc_gossip_optimistic_update_queue: 1024,
lc_bootstrap_queue: 1024, lc_bootstrap_queue: 1024,
lc_optimistic_update_queue: 512, lc_rpc_optimistic_update_queue: 512,
lc_finality_update_queue: 512, lc_rpc_finality_update_queue: 512,
lc_update_range_queue: 512, lc_update_range_queue: 512,
api_request_p0_queue: 1024, api_request_p0_queue: 1024,
api_request_p1_queue: 1024, api_request_p1_queue: 1024,
@@ -884,21 +884,16 @@ impl<E: EthSpec> BeaconProcessor<E> {
let mut gossip_attester_slashing_queue = let mut gossip_attester_slashing_queue =
FifoQueue::new(queue_lengths.gossip_attester_slashing_queue); FifoQueue::new(queue_lengths.gossip_attester_slashing_queue);
// Using a FIFO queue for light client updates to maintain sequence order.
let mut finality_update_queue = FifoQueue::new(queue_lengths.finality_update_queue);
let mut optimistic_update_queue = FifoQueue::new(queue_lengths.optimistic_update_queue);
let mut unknown_light_client_update_queue =
FifoQueue::new(queue_lengths.unknown_light_client_update_queue);
let mut unknown_block_sampling_request_queue =
FifoQueue::new(queue_lengths.unknown_block_sampling_request_queue);
// Using a FIFO queue since blocks need to be imported sequentially. // Using a FIFO queue since blocks need to be imported sequentially.
let mut rpc_block_queue = FifoQueue::new(queue_lengths.rpc_block_queue); let mut rpc_block_queue = FifoQueue::new(queue_lengths.rpc_block_queue);
let mut rpc_blob_queue = FifoQueue::new(queue_lengths.rpc_blob_queue); let mut rpc_blob_queue = FifoQueue::new(queue_lengths.rpc_blob_queue);
let mut rpc_custody_column_queue = FifoQueue::new(queue_lengths.rpc_custody_column_queue); let mut rpc_custody_column_queue = FifoQueue::new(queue_lengths.rpc_custody_column_queue);
let mut rpc_verify_data_column_queue = let mut rpc_verify_data_column_queue =
FifoQueue::new(queue_lengths.rpc_verify_data_column_queue); FifoQueue::new(queue_lengths.rpc_verify_data_column_queue);
// TODO(das): the sampling_request_queue is never read
let mut sampling_result_queue = FifoQueue::new(queue_lengths.sampling_result_queue); let mut sampling_result_queue = FifoQueue::new(queue_lengths.sampling_result_queue);
let mut unknown_block_sampling_request_queue =
FifoQueue::new(queue_lengths.unknown_block_sampling_request_queue);
let mut chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let mut chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue);
let mut backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let mut backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment);
let mut gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); let mut gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue);
@@ -917,10 +912,18 @@ impl<E: EthSpec> BeaconProcessor<E> {
let mut gossip_bls_to_execution_change_queue = let mut gossip_bls_to_execution_change_queue =
FifoQueue::new(queue_lengths.gossip_bls_to_execution_change_queue); FifoQueue::new(queue_lengths.gossip_bls_to_execution_change_queue);
// Using FIFO queues for light client updates to maintain sequence order.
let mut lc_gossip_finality_update_queue =
FifoQueue::new(queue_lengths.lc_gossip_finality_update_queue);
let mut lc_gossip_optimistic_update_queue =
FifoQueue::new(queue_lengths.lc_gossip_optimistic_update_queue);
let mut unknown_light_client_update_queue =
FifoQueue::new(queue_lengths.unknown_light_client_update_queue);
let mut lc_bootstrap_queue = FifoQueue::new(queue_lengths.lc_bootstrap_queue); let mut lc_bootstrap_queue = FifoQueue::new(queue_lengths.lc_bootstrap_queue);
let mut lc_optimistic_update_queue = let mut lc_rpc_optimistic_update_queue =
FifoQueue::new(queue_lengths.lc_optimistic_update_queue); FifoQueue::new(queue_lengths.lc_rpc_optimistic_update_queue);
let mut lc_finality_update_queue = FifoQueue::new(queue_lengths.lc_finality_update_queue); let mut lc_rpc_finality_update_queue =
FifoQueue::new(queue_lengths.lc_rpc_finality_update_queue);
let mut lc_update_range_queue = FifoQueue::new(queue_lengths.lc_update_range_queue); let mut lc_update_range_queue = FifoQueue::new(queue_lengths.lc_update_range_queue);
let mut api_request_p0_queue = FifoQueue::new(queue_lengths.api_request_p0_queue); let mut api_request_p0_queue = FifoQueue::new(queue_lengths.api_request_p0_queue);
@@ -1254,11 +1257,19 @@ impl<E: EthSpec> BeaconProcessor<E> {
} else if let Some(item) = backfill_chain_segment.pop() { } else if let Some(item) = backfill_chain_segment.pop() {
Some(item) Some(item)
// Handle light client requests. // Handle light client requests.
} else if let Some(item) = lc_gossip_finality_update_queue.pop() {
Some(item)
} else if let Some(item) = lc_gossip_optimistic_update_queue.pop() {
Some(item)
} else if let Some(item) = unknown_light_client_update_queue.pop() {
Some(item)
} else if let Some(item) = lc_bootstrap_queue.pop() { } else if let Some(item) = lc_bootstrap_queue.pop() {
Some(item) Some(item)
} else if let Some(item) = lc_optimistic_update_queue.pop() { } else if let Some(item) = lc_rpc_optimistic_update_queue.pop() {
Some(item) Some(item)
} else if let Some(item) = lc_finality_update_queue.pop() { } else if let Some(item) = lc_rpc_finality_update_queue.pop() {
Some(item)
} else if let Some(item) = lc_update_range_queue.pop() {
Some(item) Some(item)
// This statement should always be the final else statement. // This statement should always be the final else statement.
} else { } else {
@@ -1362,10 +1373,10 @@ impl<E: EthSpec> BeaconProcessor<E> {
sync_contribution_queue.push(work) sync_contribution_queue.push(work)
} }
Work::GossipLightClientFinalityUpdate { .. } => { Work::GossipLightClientFinalityUpdate { .. } => {
finality_update_queue.push(work, work_id, &self.log) lc_gossip_finality_update_queue.push(work, work_id, &self.log)
} }
Work::GossipLightClientOptimisticUpdate { .. } => { Work::GossipLightClientOptimisticUpdate { .. } => {
optimistic_update_queue.push(work, work_id, &self.log) lc_gossip_optimistic_update_queue.push(work, work_id, &self.log)
} }
Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => {
rpc_block_queue.push(work, work_id, &self.log) rpc_block_queue.push(work, work_id, &self.log)
@@ -1400,10 +1411,10 @@ impl<E: EthSpec> BeaconProcessor<E> {
lc_bootstrap_queue.push(work, work_id, &self.log) lc_bootstrap_queue.push(work, work_id, &self.log)
} }
Work::LightClientOptimisticUpdateRequest { .. } => { Work::LightClientOptimisticUpdateRequest { .. } => {
lc_optimistic_update_queue.push(work, work_id, &self.log) lc_rpc_optimistic_update_queue.push(work, work_id, &self.log)
} }
Work::LightClientFinalityUpdateRequest { .. } => { Work::LightClientFinalityUpdateRequest { .. } => {
lc_finality_update_queue.push(work, work_id, &self.log) lc_rpc_finality_update_queue.push(work, work_id, &self.log)
} }
Work::LightClientUpdatesByRangeRequest { .. } => { Work::LightClientUpdatesByRangeRequest { .. } => {
lc_update_range_queue.push(work, work_id, &self.log) lc_update_range_queue.push(work, work_id, &self.log)
@@ -1472,9 +1483,11 @@ impl<E: EthSpec> BeaconProcessor<E> {
WorkType::GossipAttesterSlashing => gossip_attester_slashing_queue.len(), WorkType::GossipAttesterSlashing => gossip_attester_slashing_queue.len(),
WorkType::GossipSyncSignature => sync_message_queue.len(), WorkType::GossipSyncSignature => sync_message_queue.len(),
WorkType::GossipSyncContribution => sync_contribution_queue.len(), WorkType::GossipSyncContribution => sync_contribution_queue.len(),
WorkType::GossipLightClientFinalityUpdate => finality_update_queue.len(), WorkType::GossipLightClientFinalityUpdate => {
lc_gossip_finality_update_queue.len()
}
WorkType::GossipLightClientOptimisticUpdate => { WorkType::GossipLightClientOptimisticUpdate => {
optimistic_update_queue.len() lc_gossip_optimistic_update_queue.len()
} }
WorkType::RpcBlock => rpc_block_queue.len(), WorkType::RpcBlock => rpc_block_queue.len(),
WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => rpc_blob_queue.len(), WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => rpc_blob_queue.len(),
@@ -1495,10 +1508,10 @@ impl<E: EthSpec> BeaconProcessor<E> {
} }
WorkType::LightClientBootstrapRequest => lc_bootstrap_queue.len(), WorkType::LightClientBootstrapRequest => lc_bootstrap_queue.len(),
WorkType::LightClientOptimisticUpdateRequest => { WorkType::LightClientOptimisticUpdateRequest => {
lc_optimistic_update_queue.len() lc_rpc_optimistic_update_queue.len()
} }
WorkType::LightClientFinalityUpdateRequest => { WorkType::LightClientFinalityUpdateRequest => {
lc_finality_update_queue.len() lc_rpc_finality_update_queue.len()
} }
WorkType::LightClientUpdatesByRangeRequest => lc_update_range_queue.len(), WorkType::LightClientUpdatesByRangeRequest => lc_update_range_queue.len(),
WorkType::ApiRequestP0 => api_request_p0_queue.len(), WorkType::ApiRequestP0 => api_request_p0_queue.len(),

View File

@@ -18,13 +18,14 @@ pub fn get_aggregate_attestation<T: BeaconChainTypes>(
endpoint_version: EndpointVersion, endpoint_version: EndpointVersion,
chain: Arc<BeaconChain<T>>, chain: Arc<BeaconChain<T>>,
) -> Result<Response<Body>, warp::reject::Rejection> { ) -> Result<Response<Body>, warp::reject::Rejection> {
if endpoint_version == V2 { let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot);
let aggregate_attestation = if fork_name.electra_enabled() {
let Some(committee_index) = committee_index else { let Some(committee_index) = committee_index else {
return Err(warp_utils::reject::custom_bad_request( return Err(warp_utils::reject::custom_bad_request(
"missing committee index".to_string(), "missing committee index".to_string(),
)); ));
}; };
let aggregate_attestation = chain chain
.get_aggregated_attestation_electra(slot, attestation_data_root, committee_index) .get_aggregated_attestation_electra(slot, attestation_data_root, committee_index)
.map_err(|e| { .map_err(|e| {
warp_utils::reject::custom_bad_request(format!( warp_utils::reject::custom_bad_request(format!(
@@ -34,8 +35,22 @@ pub fn get_aggregate_attestation<T: BeaconChainTypes>(
})? })?
.ok_or_else(|| { .ok_or_else(|| {
warp_utils::reject::custom_not_found("no matching aggregate found".to_string()) warp_utils::reject::custom_not_found("no matching aggregate found".to_string())
})?; })?
let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot); } else {
chain
.get_pre_electra_aggregated_attestation_by_slot_and_root(slot, attestation_data_root)
.map_err(|e| {
warp_utils::reject::custom_bad_request(format!(
"unable to fetch aggregate: {:?}",
e
))
})?
.ok_or_else(|| {
warp_utils::reject::custom_not_found("no matching aggregate found".to_string())
})?
};
if endpoint_version == V2 {
let fork_versioned_response = ForkVersionedResponse { let fork_versioned_response = ForkVersionedResponse {
version: Some(fork_name), version: Some(fork_name),
metadata: EmptyMetadata {}, metadata: EmptyMetadata {},
@@ -46,19 +61,7 @@ pub fn get_aggregate_attestation<T: BeaconChainTypes>(
fork_name, fork_name,
)) ))
} else if endpoint_version == V1 { } else if endpoint_version == V1 {
let aggregate_attestation = chain Ok(warp::reply::json(&GenericResponse::from(aggregate_attestation)).into_response())
.get_pre_electra_aggregated_attestation_by_slot_and_root(slot, attestation_data_root)
.map_err(|e| {
warp_utils::reject::custom_bad_request(format!(
"unable to fetch aggregate: {:?}",
e
))
})?
.map(GenericResponse::from)
.ok_or_else(|| {
warp_utils::reject::custom_not_found("no matching aggregate found".to_string())
})?;
Ok(warp::reply::json(&aggregate_attestation).into_response())
} else { } else {
return Err(unsupported_version_rejection(endpoint_version)); return Err(unsupported_version_rejection(endpoint_version));
} }

View File

@@ -3555,44 +3555,48 @@ impl ApiTester {
} }
#[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine.
pub async fn test_get_validator_aggregate_attestation(self) -> Self { pub async fn test_get_validator_aggregate_attestation_v1(self) -> Self {
if self let attestation = self
.chain .chain
.spec .head_beacon_block()
.fork_name_at_slot::<E>(self.chain.slot().unwrap()) .message()
.electra_enabled() .body()
{ .attestations()
for attestation in self.chain.naive_aggregation_pool.read().iter() { .next()
let result = self .unwrap()
.client .clone_as_attestation();
.get_validator_aggregate_attestation_v2( let result = self
attestation.data().slot, .client
attestation.data().tree_hash_root(), .get_validator_aggregate_attestation_v1(
attestation.committee_index().expect("committee index"), attestation.data().slot,
) attestation.data().tree_hash_root(),
.await )
.unwrap() .await
.unwrap() .unwrap()
.data; .unwrap()
let expected = attestation; .data;
let expected = attestation;
assert_eq!(&result, expected); assert_eq!(result, expected);
}
} else { self
let attestation = self }
.chain
.head_beacon_block() pub async fn test_get_validator_aggregate_attestation_v2(self) -> Self {
.message() let attestations = self
.body() .chain
.attestations() .naive_aggregation_pool
.next() .read()
.unwrap() .iter()
.clone_as_attestation(); .cloned()
.collect::<Vec<_>>();
for attestation in attestations {
let result = self let result = self
.client .client
.get_validator_aggregate_attestation_v1( .get_validator_aggregate_attestation_v2(
attestation.data().slot, attestation.data().slot,
attestation.data().tree_hash_root(), attestation.data().tree_hash_root(),
attestation.committee_index().expect("committee index"),
) )
.await .await
.unwrap() .unwrap()
@@ -3602,7 +3606,6 @@ impl ApiTester {
assert_eq!(result, expected); assert_eq!(result, expected);
} }
self self
} }
@@ -6775,19 +6778,36 @@ async fn get_validator_attestation_data_with_skip_slots() {
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_aggregate_attestation() { async fn get_validator_aggregate_attestation_v1() {
ApiTester::new() ApiTester::new()
.await .await
.test_get_validator_aggregate_attestation() .test_get_validator_aggregate_attestation_v1()
.await; .await;
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_aggregate_attestation_with_skip_slots() { async fn get_validator_aggregate_attestation_v2() {
ApiTester::new()
.await
.test_get_validator_aggregate_attestation_v2()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_aggregate_attestation_with_skip_slots_v1() {
ApiTester::new() ApiTester::new()
.await .await
.skip_slots(E::slots_per_epoch() * 2) .skip_slots(E::slots_per_epoch() * 2)
.test_get_validator_aggregate_attestation() .test_get_validator_aggregate_attestation_v1()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_validator_aggregate_attestation_with_skip_slots_v2() {
ApiTester::new()
.await
.skip_slots(E::slots_per_epoch() * 2)
.test_get_validator_aggregate_attestation_v2()
.await; .await;
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "boot_node" name = "boot_node"
version = "6.0.1" version = "7.0.0-beta.0"
authors = ["Sigma Prime <contact@sigmaprime.io>"] authors = ["Sigma Prime <contact@sigmaprime.io>"]
edition = { workspace = true } edition = { workspace = true }

View File

@@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!(
// NOTE: using --match instead of --exclude for compatibility with old Git // NOTE: using --match instead of --exclude for compatibility with old Git
"--match=thiswillnevermatchlol" "--match=thiswillnevermatchlol"
], ],
prefix = "Lighthouse/v6.0.1-", prefix = "Lighthouse/v7.0.0-beta.0-",
fallback = "Lighthouse/v6.0.1" fallback = "Lighthouse/v7.0.0-beta.0"
); );
/// Returns the first eight characters of the latest commit hash for this build. /// Returns the first eight characters of the latest commit hash for this build.
@@ -54,7 +54,7 @@ pub fn version_with_platform() -> String {
/// ///
/// `1.5.1` /// `1.5.1`
pub fn version() -> &'static str { pub fn version() -> &'static str {
"6.0.1" "7.0.0-beta.0"
} }
/// Returns the name of the current client running. /// Returns the name of the current client running.
@@ -71,9 +71,10 @@ mod test {
#[test] #[test]
fn version_formatting() { fn version_formatting() {
let re = let re = Regex::new(
Regex::new(r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-rc.[0-9])?(-[[:xdigit:]]{7})?\+?$") r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta).[0-9])?(-[[:xdigit:]]{7})?\+?$",
.unwrap(); )
.unwrap();
assert!( assert!(
re.is_match(VERSION), re.is_match(VERSION),
"version doesn't match regex: {}", "version doesn't match regex: {}",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lcli" name = "lcli"
description = "Lighthouse CLI (modeled after zcli)" description = "Lighthouse CLI (modeled after zcli)"
version = "6.0.1" version = "7.0.0-beta.0"
authors = ["Paul Hauner <paul@paulhauner.com>"] authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = { workspace = true } edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lighthouse" name = "lighthouse"
version = "6.0.1" version = "7.0.0-beta.0"
authors = ["Sigma Prime <contact@sigmaprime.io>"] authors = ["Sigma Prime <contact@sigmaprime.io>"]
edition = { workspace = true } edition = { workspace = true }
autotests = false autotests = false