Skip to content

Commit

Permalink
Test: Offchain workers disabled by default for container chains (#723)
Browse files Browse the repository at this point in the history
* feat:  added offchain worker pallet  that sends http requests the simple container chain template

* feat: removed unused imports and process result and added simple event emitter to offchain worker pallet

* test: added initial testing setup for offchain-logic testing

* fix: comment http function inside offchain worker pallet

* fix: fixed unused block number offchain worker function

* style: fixed toml linting

* style: commented http-related imports for offchain worker

* style: removed added empty lines in frontier container chain node toml file

* refactor: cleaned offchain worker pallet dead code and renamed it to pallet-ocw-testing

* feat: added root function that switches on and of the offchain worker

* feat: added genesis build config that switches off the offchain worker by default for offchain worker pallet

* feat: offchain worker is emitting events using raw unsigned extrinsic

* fix: offchain worker test does not timeout

* fix: removed deprecated where clause for simple container chain runtime

* test: updated moonwall tests to include cases that check for offchain worker events, both when offchain worker testing is enabled and not

* fix: removed unused import for offchain worker tests

* fix: changed offchain worker testing enabled status variable to const for offchain worker tests

* test: added check if offchain events are emitted after enabling and then disabling offchain worker using the switch extrinsic

* feat: added check if offchain testing is enbaled when validating unchecked extrinsics

* refactor: renamed switch function for offchain worker to set_offchain_worker and made it pass an additional  bool parameter that turns it on/off

* feat:  added dummy weights to offchain worker pallet

* test:  reduced  number of collators for offchain worker tests

* test: added utility function that check if event is emitted in the next N blocks to use it for offchain worker testing

* test: offchain worker test uses build-spec-dancelight-single-container.sh setup script that spawns only 1 container chain

* feat: added additional check if offchain worker is enabled inside the unsigned offchain extrinsic

* test: added offchain worker enabling-related log to checkLogsNotExist

* ci: added sha parameter to zombienet tests ci flow

---------

Co-authored-by: Aleksandar Brayanov <[email protected]>
  • Loading branch information
chexware and Aleksandar Brayanov authored Nov 6, 2024
1 parent 15fa648 commit 2fd4532
Show file tree
Hide file tree
Showing 15 changed files with 565 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/workflows/run-zombienet-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Manually run all zombienet tests
on:
workflow_dispatch:
inputs:
sha:
description: full sha to the codebase
required: true
test_name:
description: "Name of the test suite to be run (e.g. zombie_flashbox, supports regex)"
required: true
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pallet-external-validators = { path = "pallets/external-validators", default-fea
pallet-inflation-rewards = { path = "pallets/inflation-rewards", default-features = false }
pallet-initializer = { path = "pallets/initializer", default-features = false }
pallet-invulnerables = { path = "pallets/invulnerables", default-features = false }
pallet-ocw-testing = { path = "pallets/ocw-testing", default-features = false }
pallet-pooled-staking = { path = "pallets/pooled-staking", default-features = false }
pallet-registrar = { path = "pallets/registrar", default-features = false }
pallet-registrar-runtime-api = { path = "pallets/registrar/runtime-api", default-features = false }
Expand Down
6 changes: 6 additions & 0 deletions container-chains/runtime-templates/simple/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ parachains-common = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }
frame-system-benchmarking = { workspace = true, optional = true }
frame-try-runtime = { workspace = true, optional = true }

# Off-chain workers (testing)
pallet-ocw-testing = { workspace = true }

[build-dependencies]
substrate-wasm-builder = { workspace = true }

Expand Down Expand Up @@ -140,6 +144,7 @@ std = [
"pallet-message-queue/std",
"pallet-migrations/std",
"pallet-multisig/std",
"pallet-ocw-testing/std",
"pallet-proxy/std",
"pallet-root-testing/std",
"pallet-session/std",
Expand Down Expand Up @@ -247,6 +252,7 @@ try-runtime = [
"pallet-message-queue/try-runtime",
"pallet-migrations/try-runtime",
"pallet-multisig/try-runtime",
"pallet-ocw-testing/try-runtime",
"pallet-proxy/try-runtime",
"pallet-root-testing/try-runtime",
"pallet-session/try-runtime",
Expand Down
20 changes: 20 additions & 0 deletions container-chains/runtime-templates/simple/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub use sp_runtime::BuildStorage;
pub mod migrations;
pub mod weights;

pub use sp_runtime::traits::Extrinsic as ExtrinsicT;
pub use sp_runtime::{MultiAddress, Perbill, Permill};
use {
cumulus_primitives_core::AggregateMessageOrigin,
Expand Down Expand Up @@ -659,6 +660,24 @@ impl pallet_multisig::Config for Runtime {
type WeightInfo = weights::pallet_multisig::SubstrateWeight<Runtime>;
}

impl frame_system::offchain::SigningTypes for Runtime {
type Public = <Signature as sp_runtime::traits::Verify>::Signer;
type Signature = Signature;
}

impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where
RuntimeCall: From<C>,
{
type Extrinsic = UncheckedExtrinsic;
type OverarchingCall = RuntimeCall;
}

impl pallet_ocw_testing::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type UnsignedInterval = ConstU32<6>;
}

impl_tanssi_pallets_config!(Runtime);

// Create the runtime by composing the FRAME pallets that were previously configured.
Expand Down Expand Up @@ -701,6 +720,7 @@ construct_runtime!(
RootTesting: pallet_root_testing = 100,
AsyncBacking: pallet_async_backing::{Pallet, Storage} = 110,

OffchainWorker: pallet_ocw_testing::{Pallet, Call, Storage, Event<T>, ValidateUnsigned} = 120,
}
);

Expand Down
46 changes: 46 additions & 0 deletions pallets/ocw-testing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[package]
name = "pallet-ocw-testing"
authors = { workspace = true }
description = "Off-chain worker pallet used for testing"
edition = "2021"
license = "GPL-3.0-only"
version = "0.1.0"

[package.metadata.docs.rs]
targets = [ "x86_64-unknown-linux-gnu" ]

[lints]
workspace = true

[dependencies]
frame-support = { workspace = true }
frame-system = { workspace = true }
log = { workspace = true }
parity-scale-codec = { workspace = true }
scale-info = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }

[dev-dependencies]
sp-core = { workspace = true }


[features]
default = [ "std" ]
std = [
"frame-support/std",
"frame-system/std",
"log/std",
"parity-scale-codec/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
234 changes: 234 additions & 0 deletions pallets/ocw-testing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright (C) Moondance Labs Ltd.
// This file is part of Tanssi.

// Tanssi is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Tanssi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Tanssi. If not, see <http://www.gnu.org/licenses/>

#![cfg_attr(not(feature = "std"), no_std)]
use frame_system::{
self as system, ensure_none, ensure_root, offchain::SubmitTransaction,
pallet_prelude::BlockNumberFor,
};
use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction};

pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;

#[pallet::pallet]
pub struct Pallet<T>(_);

#[pallet::config]
pub trait Config:
frame_system::offchain::SendTransactionTypes<Call<Self>> + frame_system::Config
{
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

/// Number of blocks of cooldown after unsigned transaction is included.
///
/// This ensures that we only accept unsigned transactions once, every `UnsignedInterval`
/// blocks.
#[pallet::constant]
type UnsignedInterval: Get<BlockNumberFor<Self>>;
}

#[pallet::storage]
pub(super) type OffchainWorkerTestEnabled<T> = StorageValue<_, bool, ValueQuery>;

/// Defines the block when next unsigned transaction will be accepted.
///
/// To prevent spam of unsigned (and unpaid!) transactions on the network,
/// we only allow one transaction every `T::UnsignedInterval` blocks.
/// This storage entry defines when new transaction is going to be accepted.
#[pallet::storage]
pub(super) type NextUnsignedAt<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub _phantom_data: PhantomData<T>,
}

#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
<OffchainWorkerTestEnabled<T>>::put(&false);
}
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
/// Offchain worker entry point.
///
/// By implementing `fn offchain_worker` you declare a new offchain worker.
/// This function will be called when the node is fully synced and a new best block is
/// successfully imported.
/// Note that it's not guaranteed for offchain workers to run on EVERY block, there might
/// be cases where some blocks are skipped, or for some the worker runs twice (re-orgs),
/// so the code should be able to handle that.
fn offchain_worker(block_number: BlockNumberFor<T>) {
log::info!("Entering off-chain worker.");
// The entry point of your code called by off-chain worker
let res = Self::send_raw_unsigned_transaction(block_number);
if let Err(e) = res {
log::error!("Error: {}", e);
}
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Switches on or off the offchain worker
///
/// Only root (or specified authority account) should be able to switch
/// the off-chain worker on and off to avoid enabling it by default in production
#[pallet::call_index(0)]
#[pallet::weight(T::DbWeight::get().write)]
pub fn set_offchain_worker(
origin: OriginFor<T>,
is_testing_enabled: bool,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;

OffchainWorkerTestEnabled::<T>::put(is_testing_enabled);
Ok(().into())
}

/// Submits unsigned transaction that emits an event
///
/// Can be triggered only by an offchain worker
#[pallet::call_index(1)]
#[pallet::weight(T::DbWeight::get().write)]
pub fn submit_event_unsigned(
origin: OriginFor<T>,
_block_number: BlockNumberFor<T>,
) -> DispatchResultWithPostInfo {
// This ensures that the function can only be called via unsigned transaction.
ensure_none(origin)?;

ensure!(
OffchainWorkerTestEnabled::<T>::get(),
Error::<T>::OffchainWorkerNotEnabled,
);

// Increment the block number at which we expect next unsigned transaction.
let current_block = <frame_system::Pallet<T>>::block_number();

// Emits offchain event
Self::deposit_event(Event::SimpleOffchainEvent);

<NextUnsignedAt<T>>::put(current_block + T::UnsignedInterval::get());
Ok(().into())
}
}

/// Events for the pallet.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Simple offchain event
SimpleOffchainEvent,
}

#[pallet::error]
pub enum Error<T> {
OffchainWorkerNotEnabled,
}

#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;

/// Validate unsigned call to this module.
///
/// By default unsigned transactions are disallowed, but implementing the validator
/// here we make sure that some particular calls (the ones produced by offchain worker)
/// are being whitelisted and marked as valid.
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
if let Call::submit_event_unsigned { block_number } = call {
Self::validate_transaction_parameters(block_number)
} else {
InvalidTransaction::Call.into()
}
}
}
}

impl<T: Config> Pallet<T> {
/// A helper function to sign payload and send an unsigned transaction
fn send_raw_unsigned_transaction(block_number: BlockNumberFor<T>) -> Result<(), &'static str> {
// Make sure offchain worker testing is enabled
let is_offchain_worker_enabled = OffchainWorkerTestEnabled::<T>::get();
if !is_offchain_worker_enabled {
return Err("Offchain worker is not enabled");
}
// Make sure transaction can be sent
let next_unsigned_at = NextUnsignedAt::<T>::get();
if next_unsigned_at > block_number {
return Err("Too early to send unsigned transaction");
}

let call = Call::submit_event_unsigned { block_number };

SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
.map_err(|()| "Unable to submit unsigned transaction.")?;

Ok(())
}

fn validate_transaction_parameters(block_number: &BlockNumberFor<T>) -> TransactionValidity {
// Make sure offchain worker testing is enabled
let is_offchain_worker_enabled = OffchainWorkerTestEnabled::<T>::get();
if !is_offchain_worker_enabled {
return InvalidTransaction::Call.into();
}
// Now let's check if the transaction has any chance to succeed.
let next_unsigned_at = NextUnsignedAt::<T>::get();
if &next_unsigned_at > block_number {
return InvalidTransaction::Stale.into();
}
// Let's make sure to reject transactions from the future.
let current_block = <system::Pallet<T>>::block_number();
if &current_block < block_number {
return InvalidTransaction::Future.into();
}
ValidTransaction::with_tag_prefix("ExampleOffchainWorker")
// We set base priority to 2**20 and hope it's included before any other
// transactions in the pool. Next we tweak the priority depending on how much
// it differs from the current average. (the more it differs the more priority it
// has).
.priority(2u64.pow(20))
// This transaction does not require anything else to go before into the pool.
// In theory we could require `previous_unsigned_at` transaction to go first,
// but it's not necessary in our case.
//.and_requires()
// We set the `provides` tag to be the same as `next_unsigned_at`. This makes
// sure only one transaction produced after `next_unsigned_at` will ever
// get to the transaction pool and will end up in the block.
// We can still have multiple transactions compete for the same "spot",
// and the one with higher priority will replace other one in the pool.
.and_provides(next_unsigned_at)
// The transaction is only valid for next 5 blocks. After that it's
// going to be revalidated by the pool.
.longevity(6)
// It's fine to propagate that transaction to other peers, which means it can be
// created even by nodes that don't produce blocks.
// Note that sometimes it's better to keep it for yourself (if you are the block
// producer), since for instance in some schemes others may copy your solution and
// claim a reward.
.propagate(true)
.build()
}
}
Loading

0 comments on commit 2fd4532

Please sign in to comment.