mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
test: Integrate moonwall (#185)
### Description This PR introduces the **Moonwall** end-to-end (E2E) testing framework. The primary motivation for this is to enable the porting of existing Mobeam tests into the `DataHaven` repository. ### Key Changes * **Node Manual Sealing:** * Introduced a `--sealing=manual` flag for the `datahaven-node`. When enabled, blocks are only produced on demand via an RPC call. This is the core mechanism that allows for deterministic tests. * **Moonwall Framework Integration:** * Added `@moonwall/cli` and `@moonwall/util` dependencies to the `test/package.json`. * A new `test/moonwall.config.json` file configures the test environment, defining how Moonwall should launch the `datahaven-node` with the manual sealing flag. * Added a `moonwall:test` script to `package.json` for running the tests. * **CI Workflow:** * A new reusable workflow, `.github/workflows/task-moonwall-tests.yml`, has been created to handle the setup, execution, and reporting of Moonwall tests. * The main `CI.yml` now includes a `moonwall-tests` job that runs after the `build-operator` job, ensuring it always tests the correct, freshly-built binary. * **Example Test Suite:** * A new test suite, `test/datahaven/suites/dev/test-block.ts`, had been copied from moonbeam. ### How to Run Locally 1. Navigate to the `test` directory. 2. Install dependencies: `bun install` 3. Run the tests: `bun run moonwall:test` --------- Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
This commit is contained in:
parent
066a416349
commit
17c706dc64
13 changed files with 2455 additions and 153 deletions
6
.github/workflows/CI.yml
vendored
6
.github/workflows/CI.yml
vendored
|
|
@ -49,6 +49,12 @@ jobs:
|
|||
with:
|
||||
binary-hash: ${{ needs.build-operator.outputs.binary-hash }}
|
||||
|
||||
moonwall-tests:
|
||||
needs: [build-operator]
|
||||
uses: ./.github/workflows/task-moonwall-tests.yml
|
||||
with:
|
||||
binary-hash: ${{ needs.build-operator.outputs.binary-hash }}
|
||||
|
||||
# Third Tier - E2E tests depend on docker build
|
||||
e2e-tests:
|
||||
needs: [docker-build]
|
||||
|
|
|
|||
70
.github/workflows/task-moonwall-tests.yml
vendored
Normal file
70
.github/workflows/task-moonwall-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
name: Moonwall Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
binary-hash:
|
||||
description: "Hash of the built operator binary uploaded as artifact"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
moonwall:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download operator binary artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: datahaven-node-${{ inputs.binary-hash }}
|
||||
path: operator/target/x86_64-unknown-linux-gnu/release
|
||||
|
||||
- name: Prepare operator binary in expected path
|
||||
run: |
|
||||
mkdir -p ../operator/target/release
|
||||
cp ../operator/target/x86_64-unknown-linux-gnu/release/datahaven-node ../operator/target/release/datahaven-node
|
||||
chmod +x ../operator/target/release/datahaven-node
|
||||
|
||||
timeout 20 ../operator/target/release/datahaven-node \
|
||||
--dev \
|
||||
--no-telemetry \
|
||||
--unsafe-force-node-key-generation \
|
||||
--reserved-only \
|
||||
--no-grandpa \
|
||||
--no-prometheus \
|
||||
--sealing=manual 2>&1 | head -50 || echo "Node startup test completed"
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run Moonwall tests
|
||||
run: bun run moonwall:test
|
||||
|
||||
- name: Upload Moonwall reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: moonwall-reports
|
||||
path: |
|
||||
test/tmp/testResults.json
|
||||
test/reports/**
|
||||
test/**/*.log
|
||||
retention-days: 1
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
"!**/tmp/*",
|
||||
"!**/*.spec.json",
|
||||
"!**/.papi/descriptors/**/*",
|
||||
"!**/contract-bindings/**/*"
|
||||
"!**/contract-bindings/**/*",
|
||||
"!**/html/**/*"
|
||||
]
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
use datahaven_runtime_common::Block;
|
||||
use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE_HARDWARE};
|
||||
use sc_cli::SubstrateCli;
|
||||
use sc_service::DatabaseSource;
|
||||
use sc_service::{ChainType, DatabaseSource};
|
||||
use serde::Deserialize;
|
||||
use shc_client::builder::{
|
||||
BlockchainServiceOptions, BspChargeFeesOptions, BspMoveBucketOptions, BspSubmitProofOptions,
|
||||
|
|
@ -331,6 +331,17 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
};
|
||||
|
||||
runner.run_node_until_exit(|config| async move {
|
||||
let sealing_mode = match (cli.sealing, config.chain_spec.chain_type()) {
|
||||
(Some(mode), ChainType::Development) => Some(mode),
|
||||
(Some(_), _) => {
|
||||
log::warn!(
|
||||
"`--sealing` is only supported on development chains; ignoring."
|
||||
);
|
||||
None
|
||||
}
|
||||
(None, _) => None,
|
||||
};
|
||||
|
||||
match config.network.network_backend {
|
||||
// TODO: Litep2p becomes standard with Polkadot SDK stable2412-7 (should move None to other arm)
|
||||
// cfr. https://github.com/paritytech/polkadot-sdk/releases/tag/polkadot-stable2412-7
|
||||
|
|
@ -347,6 +358,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -361,6 +373,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -375,6 +388,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -394,6 +408,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -408,6 +423,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -422,6 +438,7 @@ pub fn run() -> sc_cli::Result<()> {
|
|||
provider_options,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
//! Service and ServiceFactory implementation. Specialized wrapper over substrate service.
|
||||
|
||||
use crate::cli::ProviderType;
|
||||
use crate::cli::StorageLayer;
|
||||
use crate::cli::{ProviderType, Sealing, StorageLayer};
|
||||
use crate::command::ProviderOptions;
|
||||
use crate::eth::{
|
||||
new_frontier_partial, spawn_frontier_tasks, BackendType, FrontierBackend,
|
||||
|
|
@ -14,16 +13,22 @@ use datahaven_runtime_common::{AccountId, Balance, Block, BlockNumber, Hash, Non
|
|||
use fc_consensus::FrontierBlockImport;
|
||||
use fc_db::DatabaseSource;
|
||||
use fc_storage::StorageOverride;
|
||||
use futures::channel::mpsc;
|
||||
use futures::FutureExt;
|
||||
use sc_client_api::{AuxStore, Backend, BlockBackend, StateBackend, StorageProvider};
|
||||
use sc_consensus_babe::ImportQueueParams;
|
||||
use sc_consensus_grandpa::SharedVoterState;
|
||||
use sc_consensus_manual_seal::consensus::babe::BabeConsensusDataProvider;
|
||||
use sc_consensus_manual_seal::rpc::EngineCommand;
|
||||
use sc_consensus_manual_seal::{self, InstantSealParams, ManualSealParams};
|
||||
use sc_executor::{HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY};
|
||||
use sc_network::request_responses::IncomingRequest;
|
||||
use sc_network::service::traits::NetworkService;
|
||||
use sc_network::ProtocolName;
|
||||
use sc_service::RpcHandlers;
|
||||
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig};
|
||||
use sc_service::{
|
||||
error::Error as ServiceError, ChainType, Configuration, TaskManager, WarpSyncConfig,
|
||||
};
|
||||
use sc_telemetry::{Telemetry, TelemetryWorker};
|
||||
use sc_transaction_pool::BasicPool;
|
||||
use sc_transaction_pool_api::OffchainTransactionPoolFactory;
|
||||
|
|
@ -49,6 +54,7 @@ use sp_keystore::KeystorePtr;
|
|||
use sp_runtime::traits::BlakeTwo256;
|
||||
use sp_runtime::SaturatedConversion;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::{default::Default, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
pub(crate) type FullClient<RuntimeApi> = sc_service::TFullClient<
|
||||
|
|
@ -78,6 +84,11 @@ type FullBeefyBlockImport<InnerBlockImport, AuthorityId, RuntimeApi> =
|
|||
/// imported and generated.
|
||||
const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512;
|
||||
|
||||
// Mock timestamp used for manual/instant sealing in dev mode, similar to Moonbeam.
|
||||
// Each new block will advance the timestamp by one slot duration to satisfy
|
||||
// pallet_timestamp MinimumPeriod checks when sealing back-to-back.
|
||||
static MOCK_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(crate) trait FullRuntimeApi:
|
||||
sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>
|
||||
+ sp_api::Metadata<Block>
|
||||
|
|
@ -230,6 +241,27 @@ where
|
|||
Ok(frontier_backend)
|
||||
}
|
||||
|
||||
fn build_babe_inherent_providers(
|
||||
slot_duration: sp_consensus_babe::SlotDuration,
|
||||
) -> (
|
||||
sp_consensus_babe::inherents::InherentDataProvider,
|
||||
sp_timestamp::InherentDataProvider,
|
||||
) {
|
||||
// In manual/instant sealing we want to advance time deterministically per block
|
||||
// to satisfy `pallet_timestamp` MinimumPeriod without sleeping. We increment a
|
||||
// static counter by one slot each time and use that value as the timestamp.
|
||||
let increment = slot_duration.as_millis();
|
||||
let next_ts = MOCK_TIMESTAMP
|
||||
.fetch_add(increment, Ordering::SeqCst)
|
||||
.saturating_add(increment);
|
||||
let timestamp = sp_timestamp::InherentDataProvider::new(sp_timestamp::Timestamp::new(next_ts));
|
||||
let slot = sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
*timestamp,
|
||||
slot_duration,
|
||||
);
|
||||
(slot, timestamp)
|
||||
}
|
||||
|
||||
pub fn new_partial<Runtime, RuntimeApi>(
|
||||
config: &Configuration,
|
||||
eth_config: &mut EthConfiguration,
|
||||
|
|
@ -330,16 +362,10 @@ where
|
|||
justification_import: Some(Box::new(grandpa_block_import.clone())),
|
||||
client: client.clone(),
|
||||
select_chain: select_chain.clone(),
|
||||
create_inherent_data_providers: move |_, ()| async move {
|
||||
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
|
||||
|
||||
let slot =
|
||||
sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
*timestamp,
|
||||
slot_duration,
|
||||
);
|
||||
|
||||
Ok((slot, timestamp))
|
||||
create_inherent_data_providers: move |_, ()| {
|
||||
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
|
||||
build_babe_inherent_providers(slot_duration),
|
||||
))
|
||||
},
|
||||
spawner: &task_manager.spawn_essential_handle(),
|
||||
registry: config.prometheus_registry(),
|
||||
|
|
@ -385,6 +411,7 @@ pub async fn new_full_impl<
|
|||
provider_options: Option<ProviderOptions>,
|
||||
indexer_options: Option<IndexerOptions>,
|
||||
fisherman_options: Option<FishermanOptions>,
|
||||
sealing: Option<Sealing>,
|
||||
) -> Result<TaskManager, ServiceError>
|
||||
where
|
||||
Runtime: shc_common::traits::StorageEnableRuntime<RuntimeApi = RuntimeApi>,
|
||||
|
|
@ -415,6 +442,24 @@ where
|
|||
),
|
||||
} = new_partial::<Runtime, RuntimeApi>(&config, &mut eth_config)?;
|
||||
|
||||
let role = config.role;
|
||||
let mut sealing = match sealing {
|
||||
Some(_) if !matches!(config.chain_spec.chain_type(), ChainType::Development) => {
|
||||
log::warn!("Manual sealing is only available for development chains; disabling.");
|
||||
None
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
|
||||
if sealing.is_some() && !role.is_authority() {
|
||||
log::warn!(
|
||||
"Manual sealing requested but the node is not running as an authority; disabling."
|
||||
);
|
||||
sealing = None;
|
||||
}
|
||||
|
||||
let is_authority = role.is_authority();
|
||||
|
||||
let FrontierPartialComponents {
|
||||
filter_pool,
|
||||
fee_history_cache,
|
||||
|
|
@ -543,11 +588,10 @@ where
|
|||
)
|
||||
.await?;
|
||||
|
||||
let role = config.role;
|
||||
let force_authoring = config.force_authoring;
|
||||
let backoff_authoring_blocks: Option<()> = None;
|
||||
let name = config.network.node_name.clone();
|
||||
let enable_grandpa = !config.disable_grandpa;
|
||||
let enable_grandpa = sealing.is_none() && !config.disable_grandpa;
|
||||
let prometheus_registry = config.prometheus_registry().cloned();
|
||||
let overrides = Arc::new(StorageOverrideHandler::new(client.clone()));
|
||||
|
||||
|
|
@ -559,6 +603,15 @@ where
|
|||
prometheus_registry.clone(),
|
||||
));
|
||||
|
||||
let mut manual_commands_stream: Option<mpsc::Receiver<EngineCommand<Hash>>> = None;
|
||||
let command_sink = if matches!(sealing, Some(Sealing::Manual)) {
|
||||
let (sink, stream) = mpsc::channel::<EngineCommand<Hash>>(1000);
|
||||
manual_commands_stream = Some(stream);
|
||||
Some(sink)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Sinks for pubsub notifications.
|
||||
// Everytime a new subscription is created, a new mpsc channel is added to the sink pool.
|
||||
// The MappingSyncWorker sends through the channel on block import and the subscription emits a notification to the subscriber on receiving a message through this channel.
|
||||
|
|
@ -619,8 +672,8 @@ where
|
|||
filter_pool: filter_pool.clone(),
|
||||
block_data_cache: block_data_cache.clone(),
|
||||
overrides: overrides.clone(),
|
||||
is_authority: false,
|
||||
command_sink: None,
|
||||
is_authority: is_authority.clone(),
|
||||
command_sink: command_sink.clone(),
|
||||
backend: backend.clone(),
|
||||
frontier_backend: match &*frontier_backend {
|
||||
fc_db::Backend::KeyValue(b) => b.clone(),
|
||||
|
|
@ -648,47 +701,122 @@ where
|
|||
telemetry: telemetry.as_mut(),
|
||||
})?;
|
||||
|
||||
if role.is_authority() {
|
||||
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
|
||||
task_manager.spawn_handle(),
|
||||
client.clone(),
|
||||
transaction_pool.clone(),
|
||||
prometheus_registry.as_ref(),
|
||||
telemetry.as_ref().map(|x| x.handle()),
|
||||
);
|
||||
if is_authority {
|
||||
if let Some(mode) = sealing {
|
||||
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
|
||||
task_manager.spawn_handle(),
|
||||
client.clone(),
|
||||
transaction_pool.clone(),
|
||||
prometheus_registry.as_ref(),
|
||||
telemetry.as_ref().map(|x| x.handle()),
|
||||
);
|
||||
|
||||
let slot_duration = babe_link.clone().config().slot_duration();
|
||||
let babe_config = sc_consensus_babe::BabeParams {
|
||||
keystore: keystore_container.keystore(),
|
||||
client: client.clone(),
|
||||
select_chain,
|
||||
env: proposer_factory,
|
||||
block_import,
|
||||
sync_oracle: sync_service.clone(),
|
||||
justification_sync_link: sync_service.clone(),
|
||||
create_inherent_data_providers: move |_, ()| async move {
|
||||
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
|
||||
let slot =
|
||||
sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
|
||||
*timestamp,
|
||||
slot_duration,
|
||||
);
|
||||
Ok((slot, timestamp))
|
||||
},
|
||||
force_authoring,
|
||||
backoff_authoring_blocks,
|
||||
babe_link,
|
||||
block_proposal_slot_portion: sc_consensus_babe::SlotProportion::new(0.5),
|
||||
max_block_proposal_slot_portion: None,
|
||||
telemetry: telemetry.as_ref().map(|x| x.handle()),
|
||||
};
|
||||
let slot_duration = babe_link.config().slot_duration();
|
||||
let epoch_changes = babe_link.epoch_changes().clone();
|
||||
let authorities = babe_link.config().authorities.clone();
|
||||
let keystore = keystore_container.keystore();
|
||||
let client_for_consensus = client.clone();
|
||||
let consensus_data_provider = move || {
|
||||
BabeConsensusDataProvider::new(
|
||||
client_for_consensus.clone(),
|
||||
keystore.clone(),
|
||||
epoch_changes.clone(),
|
||||
authorities.clone(),
|
||||
)
|
||||
.map(|provider| Box::new(provider) as _)
|
||||
.map_err(|e| ServiceError::Other(e.to_string()))
|
||||
};
|
||||
|
||||
let babe = sc_consensus_babe::start_babe(babe_config)?;
|
||||
task_manager.spawn_essential_handle().spawn_blocking(
|
||||
"babe-proposer",
|
||||
Some("block-authoring"),
|
||||
babe,
|
||||
);
|
||||
let create_inherent_data_providers = move |_, ()| {
|
||||
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
|
||||
build_babe_inherent_providers(slot_duration),
|
||||
))
|
||||
};
|
||||
|
||||
match mode {
|
||||
Sealing::Manual => {
|
||||
let commands_stream = manual_commands_stream.take().ok_or_else(|| {
|
||||
ServiceError::Other(
|
||||
"Manual sealing requested but command channel is unavailable".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let future = sc_consensus_manual_seal::run_manual_seal(ManualSealParams {
|
||||
block_import,
|
||||
env: proposer_factory,
|
||||
client: client.clone(),
|
||||
pool: transaction_pool.clone(),
|
||||
commands_stream,
|
||||
select_chain,
|
||||
consensus_data_provider: Some(consensus_data_provider()?),
|
||||
create_inherent_data_providers,
|
||||
});
|
||||
|
||||
task_manager.spawn_essential_handle().spawn_blocking(
|
||||
"manual-seal",
|
||||
Some("block-authoring"),
|
||||
future,
|
||||
);
|
||||
}
|
||||
Sealing::Instant => {
|
||||
let future = sc_consensus_manual_seal::run_instant_seal(InstantSealParams {
|
||||
block_import,
|
||||
env: proposer_factory,
|
||||
client: client.clone(),
|
||||
pool: transaction_pool.clone(),
|
||||
select_chain,
|
||||
consensus_data_provider: Some(consensus_data_provider()?),
|
||||
create_inherent_data_providers,
|
||||
});
|
||||
|
||||
task_manager.spawn_essential_handle().spawn_blocking(
|
||||
"manual-seal",
|
||||
Some("block-authoring"),
|
||||
future,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Manual sealing enabled (mode: {:?})", mode);
|
||||
} else {
|
||||
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
|
||||
task_manager.spawn_handle(),
|
||||
client.clone(),
|
||||
transaction_pool.clone(),
|
||||
prometheus_registry.as_ref(),
|
||||
telemetry.as_ref().map(|x| x.handle()),
|
||||
);
|
||||
|
||||
let slot_duration = babe_link.clone().config().slot_duration();
|
||||
let create_inherent_data_providers = move |_, ()| {
|
||||
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
|
||||
build_babe_inherent_providers(slot_duration),
|
||||
))
|
||||
};
|
||||
let babe_config = sc_consensus_babe::BabeParams {
|
||||
keystore: keystore_container.keystore(),
|
||||
client: client.clone(),
|
||||
select_chain,
|
||||
env: proposer_factory,
|
||||
block_import,
|
||||
sync_oracle: sync_service.clone(),
|
||||
justification_sync_link: sync_service.clone(),
|
||||
create_inherent_data_providers,
|
||||
force_authoring,
|
||||
backoff_authoring_blocks,
|
||||
babe_link,
|
||||
block_proposal_slot_portion: sc_consensus_babe::SlotProportion::new(0.5),
|
||||
max_block_proposal_slot_portion: None,
|
||||
telemetry: telemetry.as_ref().map(|x| x.handle()),
|
||||
};
|
||||
|
||||
let babe = sc_consensus_babe::start_babe(babe_config)?;
|
||||
task_manager.spawn_essential_handle().spawn_blocking(
|
||||
"babe-proposer",
|
||||
Some("block-authoring"),
|
||||
babe,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if enable_grandpa {
|
||||
|
|
@ -748,39 +876,43 @@ where
|
|||
};
|
||||
|
||||
// beefy is enabled if its notification service exists
|
||||
if let Some(notification_service) = beefy_notification_service {
|
||||
let justifications_protocol_name = beefy_on_demand_justifications_handler.protocol_name();
|
||||
let network_params = sc_consensus_beefy::BeefyNetworkParams {
|
||||
network: Arc::new(network.clone()),
|
||||
sync: sync_service.clone(),
|
||||
gossip_protocol_name: beefy_gossip_proto_name,
|
||||
justifications_protocol_name,
|
||||
notification_service,
|
||||
_phantom: core::marker::PhantomData::<Block>,
|
||||
};
|
||||
let payload_provider = sp_consensus_beefy::mmr::MmrRootProvider::new(client.clone());
|
||||
let beefy_params = sc_consensus_beefy::BeefyParams {
|
||||
client: client.clone(),
|
||||
backend: backend.clone(),
|
||||
payload_provider,
|
||||
runtime: client.clone(),
|
||||
key_store: keystore_opt.clone(),
|
||||
network_params,
|
||||
min_block_delta: 8,
|
||||
prometheus_registry: prometheus_registry.clone(),
|
||||
links: beefy_voter_links,
|
||||
on_demand_justifications_handler: beefy_on_demand_justifications_handler,
|
||||
is_authority: role.is_authority(),
|
||||
};
|
||||
if sealing.is_none() {
|
||||
if let Some(notification_service) = beefy_notification_service {
|
||||
let justifications_protocol_name =
|
||||
beefy_on_demand_justifications_handler.protocol_name();
|
||||
let network_params = sc_consensus_beefy::BeefyNetworkParams {
|
||||
network: Arc::new(network.clone()),
|
||||
sync: sync_service.clone(),
|
||||
gossip_protocol_name: beefy_gossip_proto_name,
|
||||
justifications_protocol_name,
|
||||
notification_service,
|
||||
_phantom: core::marker::PhantomData::<Block>,
|
||||
};
|
||||
let payload_provider = sp_consensus_beefy::mmr::MmrRootProvider::new(client.clone());
|
||||
let beefy_params = sc_consensus_beefy::BeefyParams {
|
||||
client: client.clone(),
|
||||
backend: backend.clone(),
|
||||
payload_provider,
|
||||
runtime: client.clone(),
|
||||
key_store: keystore_opt.clone(),
|
||||
network_params,
|
||||
min_block_delta: 8,
|
||||
prometheus_registry: prometheus_registry.clone(),
|
||||
links: beefy_voter_links,
|
||||
on_demand_justifications_handler: beefy_on_demand_justifications_handler,
|
||||
is_authority: role.is_authority(),
|
||||
};
|
||||
|
||||
let gadget =
|
||||
sc_consensus_beefy::start_beefy_gadget::<_, _, _, _, _, _, _, BeefyId>(beefy_params);
|
||||
let gadget = sc_consensus_beefy::start_beefy_gadget::<_, _, _, _, _, _, _, BeefyId>(
|
||||
beefy_params,
|
||||
);
|
||||
|
||||
// BEEFY is part of consensus, if it fails we'll bring the node down with it to make sure it
|
||||
// is noticed.
|
||||
task_manager
|
||||
.spawn_essential_handle()
|
||||
.spawn_blocking("beefy-gadget", None, gadget);
|
||||
// BEEFY is part of consensus, if it fails we'll bring the node down with it to make
|
||||
// sure it is noticed.
|
||||
task_manager
|
||||
.spawn_essential_handle()
|
||||
.spawn_blocking("beefy-gadget", None, gadget);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_) = provider_options {
|
||||
|
|
@ -822,6 +954,7 @@ pub async fn new_full<
|
|||
provider_options: Option<ProviderOptions>,
|
||||
indexer_options: Option<IndexerOptions>,
|
||||
fisherman_options: Option<FishermanOptions>,
|
||||
sealing: Option<Sealing>,
|
||||
) -> Result<TaskManager, ServiceError>
|
||||
where
|
||||
Runtime: shc_common::traits::StorageEnableRuntime<RuntimeApi = RuntimeApi>,
|
||||
|
|
@ -840,6 +973,7 @@ where
|
|||
Some(provider_options),
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -850,6 +984,7 @@ where
|
|||
Some(provider_options),
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -860,6 +995,7 @@ where
|
|||
Some(provider_options),
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -870,6 +1006,7 @@ where
|
|||
Some(provider_options),
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -881,6 +1018,7 @@ where
|
|||
None,
|
||||
indexer_options,
|
||||
fisherman_options,
|
||||
sealing,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -92,8 +92,14 @@ pub fn development_config_genesis() -> Value {
|
|||
endowed_accounts.sort();
|
||||
|
||||
testnet_genesis(
|
||||
// Alice is the only authority in Dev mode
|
||||
vec![authority_keys_from_seed("Alice")],
|
||||
// Alith is the only authority in Dev mode (using Alice's session keys)
|
||||
vec![(
|
||||
alith(),
|
||||
get_from_seed::<BabeId>("Alice"),
|
||||
get_from_seed::<GrandpaId>("Alice"),
|
||||
get_from_seed::<ImOnlineId>("Alice"),
|
||||
get_from_seed::<BeefyId>("Alice"),
|
||||
)],
|
||||
// Alith is Sudo
|
||||
alith(),
|
||||
// Endowed: Alice, Bob, Charlie, Dave, Eve, Ferdie,
|
||||
|
|
|
|||
1
test/.gitignore
vendored
1
test/.gitignore
vendored
|
|
@ -36,6 +36,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||
|
||||
# Test files
|
||||
tmp/*
|
||||
html/
|
||||
|
||||
# Local CLAUDE configuration
|
||||
CLAUDE.local.md
|
||||
1960
test/bun.lock
1960
test/bun.lock
File diff suppressed because it is too large
Load diff
53
test/datahaven/helpers/constants.ts
Normal file
53
test/datahaven/helpers/constants.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { GenericContext } from "@moonwall/cli";
|
||||
|
||||
/**
|
||||
* Class allowing to store multiple value for a runtime constant based on the runtime version
|
||||
*/
|
||||
class RuntimeConstant<T> {
|
||||
private values: { [version: number]: T };
|
||||
|
||||
/*
|
||||
* Get the expected value for a given runtime version. Lookup for the closest smaller runtime
|
||||
*/
|
||||
get(runtimeVersion: number): T {
|
||||
const versions = Object.keys(this.values).map(Number); // slow but easier to maintain
|
||||
let value: T | undefined;
|
||||
for (let i = 0; i < versions.length; i++) {
|
||||
if (versions[i] > runtimeVersion) {
|
||||
break;
|
||||
}
|
||||
value = this.values[versions[i]];
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Builds RuntimeConstant with single or multiple values
|
||||
constructor(values: { [version: number]: T } | T) {
|
||||
if (values instanceof Object) {
|
||||
this.values = values;
|
||||
} else {
|
||||
this.values = { 0: values };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fees and gas limits
|
||||
export const RUNTIME_CONSTANTS = {
|
||||
"DATAHAVEN-STAGENET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n)
|
||||
},
|
||||
"DATAHAVEN-MAINNET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n)
|
||||
},
|
||||
"DATAHAVEN-TESTNET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n)
|
||||
}
|
||||
};
|
||||
|
||||
type ConstantStoreType = (typeof RUNTIME_CONSTANTS)["DATAHAVEN-STAGENET"];
|
||||
|
||||
export function ConstantStore(context: GenericContext): ConstantStoreType {
|
||||
const runtime = context.polkadotJs().consts.system.version.specName.toUpperCase();
|
||||
console.log("runtime", runtime);
|
||||
return RUNTIME_CONSTANTS[runtime];
|
||||
}
|
||||
1
test/datahaven/helpers/index.ts
Normal file
1
test/datahaven/helpers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./constants";
|
||||
92
test/datahaven/suites/dev/test-block.ts
Normal file
92
test/datahaven/suites/dev/test-block.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
|
||||
import { ALITH_ADDRESS } from "@moonwall/util";
|
||||
import { ConstantStore } from "../../helpers";
|
||||
|
||||
describeSuite({
|
||||
id: "D010101",
|
||||
title: "Block 1",
|
||||
foundationMethods: "dev",
|
||||
testCases: ({ context, it }) => {
|
||||
let specVersion: number;
|
||||
beforeAll(async () => {
|
||||
await context.createBlock();
|
||||
specVersion = (await context.polkadotJs().runtimeVersion.specVersion).toNumber();
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T01",
|
||||
title: "should be at block 1",
|
||||
test: async () => {
|
||||
expect(await context.viem().getBlockNumber()).to.equal(1n);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T02",
|
||||
title: "should have valid timestamp after block production",
|
||||
test: async () => {
|
||||
// Seal a new block manually
|
||||
await context.createBlock();
|
||||
|
||||
// Originally, this test required the timestamp be in the last five minutes.
|
||||
// This requirement doesn't make sense when we forge timestamps in manual seal.
|
||||
const block = await context.viem().getBlock({ blockTag: "latest" });
|
||||
const next5Minutes = BigInt(Math.floor(Date.now() / 1000 + 300));
|
||||
expect(block.timestamp).toBeGreaterThan(0n);
|
||||
expect(block.timestamp).toBeLessThan(next5Minutes);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T03",
|
||||
title: "should contain block information",
|
||||
test: async () => {
|
||||
const block = await context.viem().getBlock({ blockTag: "latest" });
|
||||
expect(block).to.include({
|
||||
author: ALITH_ADDRESS.toLocaleLowerCase(),
|
||||
difficulty: 0n,
|
||||
extraData: "0x",
|
||||
gasLimit: ConstantStore(context).GAS_LIMIT.get(specVersion),
|
||||
gasUsed: 0n,
|
||||
logsBloom: `0x${"0".repeat(512)}`,
|
||||
miner: ALITH_ADDRESS.toLocaleLowerCase(),
|
||||
number: 2n,
|
||||
receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||
totalDifficulty: 0n,
|
||||
transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
});
|
||||
|
||||
expect(block.transactions).to.be.a("array").empty;
|
||||
expect(block.uncles).to.be.a("array").empty;
|
||||
expect(block.nonce).to.be.eq("0x0000000000000000");
|
||||
expect(block.hash).to.be.a("string").lengthOf(66);
|
||||
expect(block.parentHash).to.be.a("string").lengthOf(66);
|
||||
expect(block.timestamp).to.be.a("bigint");
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T04",
|
||||
title: "should be accessible by hash",
|
||||
test: async () => {
|
||||
const latestBlock = await context.viem().getBlock({ blockTag: "latest" });
|
||||
if (!latestBlock.hash) {
|
||||
throw new Error("Latest block hash is null");
|
||||
}
|
||||
const block = await context.viem().getBlock({ blockHash: latestBlock.hash });
|
||||
expect(block.hash).toBe(latestBlock.hash);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T05",
|
||||
title: "should be accessible by number",
|
||||
test: async () => {
|
||||
const latestBlock = await context.viem().getBlock({ blockTag: "latest" });
|
||||
const block = await context.viem().getBlock({ blockNumber: latestBlock.number });
|
||||
expect(block.hash).toBe(latestBlock.hash);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
77
test/moonwall.config.json
Normal file
77
test/moonwall.config.json
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/Moonsong-Labs/moonwall/main/packages/types/config_schema.json",
|
||||
"label": "DataHaven Tests 🔷",
|
||||
"defaultTestTimeout": 120000,
|
||||
"environments": [
|
||||
{
|
||||
"name": "dev_datahaven",
|
||||
"testFileDir": [
|
||||
"suites/dev"
|
||||
],
|
||||
"include": [
|
||||
"**/*test*.ts"
|
||||
],
|
||||
"timeout": 180000,
|
||||
"multiThreads": 1,
|
||||
"envVars": [
|
||||
"DEBUG_COLORS=1",
|
||||
"RUST_BACKTRACE=1",
|
||||
"RUST_LOG=info"
|
||||
],
|
||||
"reporters": [
|
||||
"basic",
|
||||
"html",
|
||||
"json"
|
||||
],
|
||||
"reportFile": {
|
||||
"json": "./tmp/testResults.json"
|
||||
},
|
||||
"foundation": {
|
||||
"type": "dev",
|
||||
"launchSpec": [
|
||||
{
|
||||
"name": "datahaven",
|
||||
"binPath": "../operator/target/release/datahaven-node",
|
||||
"newRpcBehaviour": true,
|
||||
"options": [
|
||||
"--dev",
|
||||
"--no-telemetry",
|
||||
"--unsafe-force-node-key-generation",
|
||||
"--reserved-only",
|
||||
"--no-grandpa",
|
||||
"--no-prometheus",
|
||||
"--sealing=manual"
|
||||
],
|
||||
"ports": {
|
||||
"p2pPort": 30333,
|
||||
"rpcPort": 9944
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": [
|
||||
{
|
||||
"name": "ethers",
|
||||
"type": "ethers",
|
||||
"endpoints": [
|
||||
"ws://127.0.0.1:9944"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "viem",
|
||||
"type": "viem",
|
||||
"endpoints": [
|
||||
"ws://127.0.0.1:9944"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "solo",
|
||||
"type": "polkadotJs",
|
||||
"endpoints": [
|
||||
"ws://127.0.0.1:9944"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@
|
|||
"stop:engine": "bun cli stop --kurtosisEngine --no-datahaven --no-relayer --no-enclave",
|
||||
"test:e2e": "bun test ./suites --timeout 900000",
|
||||
"test:e2e:parallel": "bun scripts/test-parallel.ts",
|
||||
"moonwall:test": "moonwall test dev_datahaven",
|
||||
"moonwall:run": "moonwall run dev_datahaven",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"tsgo": "tsgo tsc --noEmit --pretty --skipLibCheck",
|
||||
"postinstall": "papi"
|
||||
|
|
@ -42,6 +44,8 @@
|
|||
"@commander-js/extra-typings": "^13.1.0",
|
||||
"@dotenvx/dotenvx": "^1.44.2",
|
||||
"@inquirer/prompts": "^7.5.3",
|
||||
"@moonwall/cli": "^5.15.0",
|
||||
"@moonwall/util": "^5.15.0",
|
||||
"@noble/curves": "^1.9.2",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@polkadot-api/descriptors": "file:.papi/descriptors",
|
||||
|
|
|
|||
Loading…
Reference in a new issue