feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent e4778b4576
commit 379cb741ed
9082 changed files with 997824 additions and 997542 deletions
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "pezcumulus-primitives-aura"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
description = "Core primitives for Aura in Pezcumulus"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# Bizinikiwi
pezsp-api = { workspace = true }
pezsp-consensus-aura = { workspace = true }
[features]
default = ["std"]
std = ["pezsp-api/std", "pezsp-consensus-aura/std"]
runtime-benchmarks = [
"pezsp-api/runtime-benchmarks",
"pezsp-consensus-aura/runtime-benchmarks",
]
+54
View File
@@ -0,0 +1,54 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Core primitives for Aura in Pezcumulus.
//!
//! In particular, this exposes the [`AuraUnincludedSegmentApi`] which is used to regulate
//! the behavior of Aura within a teyrchain context.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pezsp_consensus_aura::Slot;
pezsp_api::decl_runtime_apis! {
/// This runtime API is used to inform potential block authors whether they will
/// have the right to author at a slot, assuming they have claimed the slot.
///
/// In particular, this API allows Aura-based teyrchains to regulate their "unincluded segment",
/// which is the section of the head of the chain which has not yet been made available in the
/// relay chain.
///
/// When the unincluded segment is short, Aura chains will allow authors to create multiple
/// blocks per slot in order to build a backlog. When it is saturated, this API will limit
/// the amount of blocks that can be created.
///
/// Changes:
/// - Version 2: Update to `can_build_upon` to take a relay chain `Slot` instead of a teyrchain `Slot`.
#[api_version(2)]
pub trait AuraUnincludedSegmentApi {
/// Whether it is legal to extend the chain, assuming the given block is the most
/// recently included one as-of the relay parent that will be built against, and
/// the given relay chain slot.
///
/// This should be consistent with the logic the runtime uses when validating blocks to
/// avoid issues.
///
/// When the unincluded segment is empty, i.e. `included_hash == at`, where at is the block
/// whose state we are querying against, this must always return `true` as long as the slot
/// is more recent than the included block itself.
fn can_build_upon(included_hash: Block::Hash, slot: Slot) -> bool;
}
}
+52
View File
@@ -0,0 +1,52 @@
[package]
name = "pezcumulus-primitives-core"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
description = "Pezcumulus related core primitive types and traits"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bizinikiwi
pezsp-api = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-trie = { workspace = true }
# Pezkuwi
pezkuwi-core-primitives = { workspace = true }
pezkuwi-primitives = { workspace = true }
pezkuwi-teyrchain-primitives = { workspace = true }
xcm = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezkuwi-core-primitives/std",
"pezkuwi-primitives/std",
"pezkuwi-teyrchain-primitives/std",
"scale-info/std",
"pezsp-api/std",
"pezsp-runtime/std",
"pezsp-trie/std",
"tracing/std",
"xcm/std",
]
runtime-benchmarks = [
"pezkuwi-core-primitives/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"pezsp-api/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
+516
View File
@@ -0,0 +1,516 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Pezcumulus related core primitive types and traits.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec::Vec;
use codec::{Compact, Decode, DecodeAll, DecodeWithMemTracking, Encode, MaxEncodedLen};
use pezkuwi_teyrchain_primitives::primitives::HeadData;
use scale_info::TypeInfo;
use pezsp_runtime::RuntimeDebug;
/// The ref time per core in seconds.
///
/// This is the execution time each PoV gets on a core on the relay chain.
pub const REF_TIME_PER_CORE_IN_SECS: u64 = 2;
pub mod teyrchain_block_data;
pub use pezkuwi_core_primitives::InboundDownwardMessage;
pub use pezkuwi_primitives::{
AbridgedHostConfiguration, AbridgedHrmpChannel, ClaimQueueOffset, CoreSelector,
PersistedValidationData,
};
pub use pezkuwi_teyrchain_primitives::primitives::{
DmpMessageHandler, Id as ParaId, IsSystem, UpwardMessage, ValidationParams, XcmpMessageFormat,
XcmpMessageHandler,
};
pub use pezsp_runtime::{
generic::{Digest, DigestItem},
traits::Block as BlockT,
ConsensusEngineId,
};
pub use teyrchain_block_data::TeyrchainBlockData;
pub use xcm::latest::prelude::*;
/// A module that re-exports relevant relay chain definitions.
pub mod relay_chain {
pub use pezkuwi_core_primitives::*;
pub use pezkuwi_primitives::*;
}
/// An inbound HRMP message.
pub type InboundHrmpMessage = pezkuwi_primitives::InboundHrmpMessage<relay_chain::BlockNumber>;
/// And outbound HRMP message
pub type OutboundHrmpMessage = pezkuwi_primitives::OutboundHrmpMessage<ParaId>;
/// Error description of a message send failure.
#[derive(Eq, PartialEq, Copy, Clone, RuntimeDebug, Encode, Decode)]
pub enum MessageSendError {
/// The dispatch queue is full.
QueueFull,
/// There does not exist a channel for sending the message.
NoChannel,
/// The message is too big to ever fit in a channel.
TooBig,
/// Some other error.
Other,
/// There are too many channels open at once.
TooManyChannels,
}
impl From<MessageSendError> for &'static str {
fn from(e: MessageSendError) -> Self {
use MessageSendError::*;
match e {
QueueFull => "QueueFull",
NoChannel => "NoChannel",
TooBig => "TooBig",
Other => "Other",
TooManyChannels => "TooManyChannels",
}
}
}
/// The origin of an inbound message.
#[derive(
Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, Clone, Eq, PartialEq, TypeInfo, Debug,
)]
pub enum AggregateMessageOrigin {
/// The message came from the para-chain itself.
Here,
/// The message came from the relay-chain.
///
/// This is used by the DMP queue.
Parent,
/// The message came from a sibling para-chain.
///
/// This is used by the HRMP queue.
Sibling(ParaId),
}
impl From<AggregateMessageOrigin> for Location {
fn from(origin: AggregateMessageOrigin) -> Self {
match origin {
AggregateMessageOrigin::Here => Location::here(),
AggregateMessageOrigin::Parent => Location::parent(),
AggregateMessageOrigin::Sibling(id) => Location::new(1, Junction::Teyrchain(id.into())),
}
}
}
#[cfg(feature = "runtime-benchmarks")]
impl From<u32> for AggregateMessageOrigin {
fn from(x: u32) -> Self {
match x {
0 => Self::Here,
1 => Self::Parent,
p => Self::Sibling(ParaId::from(p)),
}
}
}
/// Information about an XCMP channel.
pub struct ChannelInfo {
/// The maximum number of messages that can be pending in the channel at once.
pub max_capacity: u32,
/// The maximum total size of the messages that can be pending in the channel at once.
pub max_total_size: u32,
/// The maximum message size that could be put into the channel.
pub max_message_size: u32,
/// The current number of messages pending in the channel.
/// Invariant: should be less or equal to `max_capacity`.s`.
pub msg_count: u32,
/// The total size in bytes of all message payloads in the channel.
/// Invariant: should be less or equal to `max_total_size`.
pub total_size: u32,
}
pub trait GetChannelInfo {
fn get_channel_status(id: ParaId) -> ChannelStatus;
fn get_channel_info(id: ParaId) -> Option<ChannelInfo>;
}
/// List all open outgoing channels.
pub trait ListChannelInfos {
fn outgoing_channels() -> Vec<ParaId>;
}
/// Something that should be called when sending an upward message.
pub trait UpwardMessageSender {
/// Send the given UMP message; return the expected number of blocks before the message will
/// be dispatched or an error if the message cannot be sent.
/// return the hash of the message sent
fn send_upward_message(message: UpwardMessage) -> Result<(u32, XcmHash), MessageSendError>;
/// Pre-check the given UMP message.
fn can_send_upward_message(message: &UpwardMessage) -> Result<(), MessageSendError>;
/// Ensure `[Self::send_upward_message]` is successful when called in benchmarks/tests.
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
fn ensure_successful_delivery() {}
}
impl UpwardMessageSender for () {
fn send_upward_message(_message: UpwardMessage) -> Result<(u32, XcmHash), MessageSendError> {
Err(MessageSendError::NoChannel)
}
fn can_send_upward_message(_message: &UpwardMessage) -> Result<(), MessageSendError> {
Err(MessageSendError::Other)
}
}
/// The status of a channel.
pub enum ChannelStatus {
/// Channel doesn't exist/has been closed.
Closed,
/// Channel is completely full right now.
Full,
/// Channel is ready for sending; the two parameters are the maximum size a valid message may
/// have right now, and the maximum size a message may ever have (this will generally have been
/// available during message construction, but it's possible the channel parameters changed in
/// the meantime).
Ready(usize, usize),
}
/// A means of figuring out what outbound XCMP messages should be being sent.
pub trait XcmpMessageSource {
/// Take a single XCMP message from the queue for the given `dest`, if one exists.
fn take_outbound_messages(maximum_channels: usize) -> Vec<(ParaId, Vec<u8>)>;
}
impl XcmpMessageSource for () {
fn take_outbound_messages(_maximum_channels: usize) -> Vec<(ParaId, Vec<u8>)> {
Vec::new()
}
}
/// The "quality of service" considerations for message sending.
#[derive(Eq, PartialEq, Clone, Copy, Encode, Decode, RuntimeDebug)]
pub enum ServiceQuality {
/// Ensure that this message is dispatched in the same relative order as any other messages
/// that were also sent with `Ordered`. This only guarantees message ordering on the dispatch
/// side, and not necessarily on the execution side.
Ordered,
/// Ensure that the message is dispatched as soon as possible, which could result in it being
/// dispatched before other messages which are larger and/or rely on relative ordering.
Fast,
}
/// A consensus engine ID indicating that this is a Pezcumulus Teyrchain.
pub const CUMULUS_CONSENSUS_ID: ConsensusEngineId = *b"CMLS";
/// Information about the core on the relay chain this block will be validated on.
#[derive(Clone, Debug, Decode, Encode, PartialEq, Eq)]
pub struct CoreInfo {
/// The selector that determines the actual core at `claim_queue_offset`.
pub selector: CoreSelector,
/// The claim queue offset that determines how far "into the future" the core is selected.
pub claim_queue_offset: ClaimQueueOffset,
/// The number of cores assigned to the teyrchain at `claim_queue_offset`.
pub number_of_cores: Compact<u16>,
}
/// Return value of [`CumulusDigestItem::core_info_exists_at_max_once`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoreInfoExistsAtMaxOnce {
/// Exists exactly once.
Once(CoreInfo),
/// Not found.
NotFound,
/// Found more than once.
MoreThanOnce,
}
/// Identifier for a relay chain block used by [`CumulusDigestItem`].
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
pub enum RelayBlockIdentifier {
/// The block is identified using its block hash.
ByHash(relay_chain::Hash),
/// The block is identified using its storage root and block number.
ByStorageRoot { storage_root: relay_chain::Hash, block_number: relay_chain::BlockNumber },
}
/// Consensus header digests for Pezcumulus teyrchains.
#[derive(Clone, Debug, Decode, Encode, PartialEq)]
pub enum CumulusDigestItem {
/// A digest item indicating the relay-parent a teyrchain block was built against.
#[codec(index = 0)]
RelayParent(relay_chain::Hash),
/// A digest item providing information about the core selected on the relay chain for this
/// block.
#[codec(index = 1)]
CoreInfo(CoreInfo),
}
impl CumulusDigestItem {
/// Encode this as a Bizinikiwi [`DigestItem`].
pub fn to_digest_item(&self) -> DigestItem {
match self {
Self::RelayParent(_) => DigestItem::Consensus(CUMULUS_CONSENSUS_ID, self.encode()),
Self::CoreInfo(_) => DigestItem::PreRuntime(CUMULUS_CONSENSUS_ID, self.encode()),
}
}
/// Find [`CumulusDigestItem::CoreInfo`] in the given `digest`.
///
/// If there are multiple valid digests, this returns the value of the first one.
pub fn find_core_info(digest: &Digest) -> Option<CoreInfo> {
digest.convert_first(|d| match d {
DigestItem::PreRuntime(id, val) if id == &CUMULUS_CONSENSUS_ID => {
let Ok(CumulusDigestItem::CoreInfo(core_info)) =
CumulusDigestItem::decode_all(&mut &val[..])
else {
return None;
};
Some(core_info)
},
_ => None,
})
}
/// Returns the found [`CoreInfo`] and iff [`Self::CoreInfo`] exists at max once in the given
/// `digest`.
pub fn core_info_exists_at_max_once(digest: &Digest) -> CoreInfoExistsAtMaxOnce {
let mut core_info = None;
if digest
.logs()
.iter()
.filter(|l| match l {
DigestItem::PreRuntime(CUMULUS_CONSENSUS_ID, d) => {
if let Ok(Self::CoreInfo(ci)) = Self::decode_all(&mut &d[..]) {
core_info = Some(ci);
true
} else {
false
}
},
_ => false,
})
.count() <= 1
{
core_info
.map(CoreInfoExistsAtMaxOnce::Once)
.unwrap_or(CoreInfoExistsAtMaxOnce::NotFound)
} else {
CoreInfoExistsAtMaxOnce::MoreThanOnce
}
}
/// Returns the [`RelayBlockIdentifier`] from the given `digest`.
///
/// The identifier corresponds to the relay parent used to build the teyrchain block.
pub fn find_relay_block_identifier(digest: &Digest) -> Option<RelayBlockIdentifier> {
digest.convert_first(|d| match d {
DigestItem::Consensus(id, val) if id == &CUMULUS_CONSENSUS_ID => {
let Ok(CumulusDigestItem::RelayParent(hash)) =
CumulusDigestItem::decode_all(&mut &val[..])
else {
return None;
};
Some(RelayBlockIdentifier::ByHash(hash))
},
DigestItem::Consensus(id, val) if id == &rpsr_digest::RPSR_CONSENSUS_ID => {
let Ok((storage_root, block_number)) =
rpsr_digest::RpsrType::decode_all(&mut &val[..])
else {
return None;
};
Some(RelayBlockIdentifier::ByStorageRoot {
storage_root,
block_number: block_number.into(),
})
},
_ => None,
})
}
}
///
/// If there are multiple valid digests, this returns the value of the first one, although
/// well-behaving runtimes should not produce headers with more than one.
pub fn extract_relay_parent(digest: &Digest) -> Option<relay_chain::Hash> {
digest.convert_first(|d| match d {
DigestItem::Consensus(id, val) if id == &CUMULUS_CONSENSUS_ID =>
match CumulusDigestItem::decode(&mut &val[..]) {
Ok(CumulusDigestItem::RelayParent(hash)) => Some(hash),
_ => None,
},
_ => None,
})
}
/// Utilities for handling the relay-parent storage root as a digest item.
///
/// This is not intended to be part of the public API, as it is a workaround for
/// <https://github.com/pezkuwichain/kurdistan-sdk/issues/92> via
/// <https://github.com/pezkuwichain/kurdistan-sdk/issues/169>.
///
/// Runtimes using the teyrchain-system pallet are expected to produce this digest item,
/// but will stop as soon as they are able to provide the relay-parent hash directly.
///
/// The relay-chain storage root is, in practice, a unique identifier of a block
/// in the absence of equivocations (which are slashable). This assumes that the relay chain
/// uses BABE or SASSAFRAS, because the slot and the author's VRF randomness are both included
/// in the relay-chain storage root in both cases.
///
/// Therefore, the relay-parent storage root is a suitable identifier of unique relay chain
/// blocks in low-value scenarios such as performance optimizations.
#[doc(hidden)]
pub mod rpsr_digest {
use super::{relay_chain, ConsensusEngineId, DecodeAll, Digest, DigestItem, Encode};
use codec::Compact;
/// The type used to store the relay-parent storage root and number.
pub type RpsrType = (relay_chain::Hash, Compact<relay_chain::BlockNumber>);
/// A consensus engine ID for relay-parent storage root digests.
pub const RPSR_CONSENSUS_ID: ConsensusEngineId = *b"RPSR";
/// Construct a digest item for relay-parent storage roots.
pub fn relay_parent_storage_root_item(
storage_root: relay_chain::Hash,
number: impl Into<Compact<relay_chain::BlockNumber>>,
) -> DigestItem {
DigestItem::Consensus(
RPSR_CONSENSUS_ID,
RpsrType::from((storage_root, number.into())).encode(),
)
}
/// Extract the relay-parent storage root and number from the provided header digest. Returns
/// `None` if none were found.
pub fn extract_relay_parent_storage_root(
digest: &Digest,
) -> Option<(relay_chain::Hash, relay_chain::BlockNumber)> {
digest.convert_first(|d| match d {
DigestItem::Consensus(id, val) if id == &RPSR_CONSENSUS_ID => {
let (h, n) = RpsrType::decode_all(&mut &val[..]).ok()?;
Some((h, n.0))
},
_ => None,
})
}
}
/// Information about a collation.
///
/// This was used in version 1 of the [`CollectCollationInfo`] runtime api.
#[derive(Clone, Debug, codec::Decode, codec::Encode, PartialEq)]
pub struct CollationInfoV1 {
/// Messages destined to be interpreted by the Relay chain itself.
pub upward_messages: Vec<UpwardMessage>,
/// The horizontal messages sent by the teyrchain.
pub horizontal_messages: Vec<OutboundHrmpMessage>,
/// New validation code.
pub new_validation_code: Option<relay_chain::ValidationCode>,
/// The number of messages processed from the DMQ.
pub processed_downward_messages: u32,
/// The mark which specifies the block number up to which all inbound HRMP messages are
/// processed.
pub hrmp_watermark: relay_chain::BlockNumber,
}
impl CollationInfoV1 {
/// Convert into the latest version of the [`CollationInfo`] struct.
pub fn into_latest(self, head_data: HeadData) -> CollationInfo {
CollationInfo {
upward_messages: self.upward_messages,
horizontal_messages: self.horizontal_messages,
new_validation_code: self.new_validation_code,
processed_downward_messages: self.processed_downward_messages,
hrmp_watermark: self.hrmp_watermark,
head_data,
}
}
}
/// Information about a collation.
#[derive(Clone, Debug, codec::Decode, codec::Encode, PartialEq, TypeInfo)]
pub struct CollationInfo {
/// Messages destined to be interpreted by the Relay chain itself.
pub upward_messages: Vec<UpwardMessage>,
/// The horizontal messages sent by the teyrchain.
pub horizontal_messages: Vec<OutboundHrmpMessage>,
/// New validation code.
pub new_validation_code: Option<relay_chain::ValidationCode>,
/// The number of messages processed from the DMQ.
pub processed_downward_messages: u32,
/// The mark which specifies the block number up to which all inbound HRMP messages are
/// processed.
pub hrmp_watermark: relay_chain::BlockNumber,
/// The head data, aka encoded header, of the block that corresponds to the collation.
pub head_data: HeadData,
}
pezsp_api::decl_runtime_apis! {
/// Runtime api to collect information about a collation.
///
/// Version history:
/// - Version 2: Changed [`Self::collect_collation_info`] signature
/// - Version 3: Signals to the node to use version 1 of [`TeyrchainBlockData`].
#[api_version(3)]
pub trait CollectCollationInfo {
/// Collect information about a collation.
#[changed_in(2)]
fn collect_collation_info() -> CollationInfoV1;
/// Collect information about a collation.
///
/// The given `header` is the header of the built block for that
/// we are collecting the collation info for.
fn collect_collation_info(header: &Block::Header) -> CollationInfo;
}
/// Runtime api used to access general info about a teyrchain runtime.
pub trait GetTeyrchainInfo {
/// Retrieve the teyrchain id used for runtime.
fn teyrchain_id() -> ParaId;
}
/// API to tell the node side how the relay parent should be chosen.
///
/// A larger offset indicates that the relay parent should not be the tip of the relay chain,
/// but `N` blocks behind the tip. This offset is then enforced by the runtime.
pub trait RelayParentOffsetApi {
/// Fetch the slot offset that is expected from the relay chain.
fn relay_parent_offset() -> u32;
}
/// API for teyrchain target block rate.
///
/// This runtime API allows the teyrchain runtime to communicate the target block rate
/// to the node side. The target block rate is always valid for the next relay chain slot.
///
/// The runtime can not enforce this target block rate. It only acts as a maximum, but not more.
/// In the end it depends on the collator how many blocks will be produced. If there are no cores
/// available or the collator is offline, no blocks at all will be produced.
pub trait TargetBlockRate {
/// Get the target block rate for this teyrchain.
///
/// Returns the target number of blocks per relay chain slot.
fn target_block_rate() -> u32;
}
}
@@ -0,0 +1,260 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Provides [`TeyrchainBlockData`] and its historical versions.
use alloc::vec::Vec;
use codec::{Decode, Encode};
use pezsp_runtime::traits::Block as BlockT;
use pezsp_trie::CompactProof;
/// Special prefix used by [`TeyrchainBlockData`] from version 1 and upwards to distinguish from the
/// unversioned legacy/v0 version.
const VERSIONED_TEYRCHAIN_BLOCK_DATA_PREFIX: &[u8] = b"VERSIONEDPBD";
// Struct which allows prepending bytes after reading from an input.
pub(crate) struct PrependBytesInput<'a, I> {
prepend: &'a [u8],
read: usize,
inner: &'a mut I,
}
impl<'a, I: codec::Input> codec::Input for PrependBytesInput<'a, I> {
fn remaining_len(&mut self) -> Result<Option<usize>, codec::Error> {
let remaining_compact = self.prepend.len().saturating_sub(self.read);
Ok(self.inner.remaining_len()?.map(|len| len.saturating_add(remaining_compact)))
}
fn read(&mut self, into: &mut [u8]) -> Result<(), codec::Error> {
if into.is_empty() {
return Ok(());
}
let remaining_compact = self.prepend.len().saturating_sub(self.read);
if remaining_compact > 0 {
let to_read = into.len().min(remaining_compact);
into[..to_read].copy_from_slice(&self.prepend[self.read..][..to_read]);
self.read += to_read;
if to_read < into.len() {
// Buffer not full, keep reading the inner.
self.inner.read(&mut into[to_read..])
} else {
// Buffer was filled by the bytes.
Ok(())
}
} else {
// Prepended bytes has been read, just read from inner.
self.inner.read(into)
}
}
}
/// The teyrchain block that is created by a collator.
///
/// This is send as PoV (proof of validity block) to the relay-chain validators. There it will be
/// passed to the teyrchain validation Wasm blob to be validated.
#[derive(Clone)]
pub enum TeyrchainBlockData<Block> {
V0 { block: [Block; 1], proof: CompactProof },
V1 { blocks: Vec<Block>, proof: CompactProof },
}
impl<Block: Encode> Encode for TeyrchainBlockData<Block> {
fn encode(&self) -> Vec<u8> {
match self {
Self::V0 { block, proof } => (&block[0], &proof).encode(),
Self::V1 { blocks, proof } => {
let mut res = VERSIONED_TEYRCHAIN_BLOCK_DATA_PREFIX.to_vec();
1u8.encode_to(&mut res);
blocks.encode_to(&mut res);
proof.encode_to(&mut res);
res
},
}
}
}
impl<Block: Decode> Decode for TeyrchainBlockData<Block> {
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
let mut prefix = [0u8; VERSIONED_TEYRCHAIN_BLOCK_DATA_PREFIX.len()];
input.read(&mut prefix)?;
if prefix == VERSIONED_TEYRCHAIN_BLOCK_DATA_PREFIX {
match input.read_byte()? {
1 => {
let blocks = Vec::<Block>::decode(input)?;
let proof = CompactProof::decode(input)?;
Ok(Self::V1 { blocks, proof })
},
_ => Err("Unknown `TeyrchainBlockData` version".into()),
}
} else {
let mut input = PrependBytesInput { prepend: &prefix, read: 0, inner: input };
let block = Block::decode(&mut input)?;
let proof = CompactProof::decode(&mut input)?;
Ok(Self::V0 { block: [block], proof })
}
}
}
impl<Block> TeyrchainBlockData<Block> {
/// Creates a new instance of `Self`.
pub fn new(blocks: Vec<Block>, proof: CompactProof) -> Self {
Self::V1 { blocks, proof }
}
/// Returns references to the stored blocks.
pub fn blocks(&self) -> &[Block] {
match self {
Self::V0 { block, .. } => &block[..],
Self::V1 { blocks, .. } => &blocks,
}
}
/// Returns mutable references to the stored blocks.
pub fn blocks_mut(&mut self) -> &mut [Block] {
match self {
Self::V0 { ref mut block, .. } => block,
Self::V1 { ref mut blocks, .. } => blocks,
}
}
/// Returns the stored blocks.
pub fn into_blocks(self) -> Vec<Block> {
match self {
Self::V0 { block, .. } => block.into_iter().collect(),
Self::V1 { blocks, .. } => blocks,
}
}
/// Returns a reference to the stored proof.
pub fn proof(&self) -> &CompactProof {
match self {
Self::V0 { proof, .. } => &proof,
Self::V1 { proof, .. } => proof,
}
}
/// Deconstruct into the inner parts.
pub fn into_inner(self) -> (Vec<Block>, CompactProof) {
match self {
Self::V0 { block, proof } => (block.into_iter().collect(), proof),
Self::V1 { blocks, proof } => (blocks, proof),
}
}
}
impl<Block: BlockT> TeyrchainBlockData<Block> {
/// Log the size of the individual components (header, extrinsics, storage proof) as info.
pub fn log_size_info(&self) {
tracing::info!(
target: "pezcumulus",
header_kb = %self.blocks().iter().map(|b| b.header().encoded_size()).sum::<usize>() as f64 / 1024f64,
extrinsics_kb = %self.blocks().iter().map(|b| b.extrinsics().encoded_size()).sum::<usize>() as f64 / 1024f64,
storage_proof_kb = %self.proof().encoded_size() as f64 / 1024f64,
"PoV size",
);
}
/// Converts into [`TeyrchainBlockData::V0`].
///
/// Returns `None` if there is not exactly one block.
pub fn as_v0(&self) -> Option<Self> {
match self {
Self::V0 { .. } => Some(self.clone()),
Self::V1 { blocks, proof } => {
if blocks.len() != 1 {
return None;
}
blocks
.first()
.map(|block| Self::V0 { block: [block.clone()], proof: proof.clone() })
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezsp_runtime::testing::*;
#[derive(codec::Encode, codec::Decode, Clone, PartialEq, Debug)]
struct TeyrchainBlockDataV0<B: BlockT> {
/// The header of the teyrchain block.
pub header: B::Header,
/// The extrinsics of the teyrchain block.
pub extrinsics: alloc::vec::Vec<B::Extrinsic>,
/// The data that is required to emulate the storage accesses executed by all extrinsics.
pub storage_proof: pezsp_trie::CompactProof,
}
type TestExtrinsic = TestXt<MockCallU64, ()>;
type TestBlock = Block<TestExtrinsic>;
#[test]
fn decoding_encoding_v0_works() {
let v0 = TeyrchainBlockDataV0::<TestBlock> {
header: Header::new_from_number(10),
extrinsics: vec![
TestExtrinsic::new_bare(MockCallU64(10)),
TestExtrinsic::new_bare(MockCallU64(100)),
],
storage_proof: CompactProof { encoded_nodes: vec![vec![10u8; 200], vec![20u8; 30]] },
};
let encoded = v0.encode();
let decoded = TeyrchainBlockData::<TestBlock>::decode(&mut &encoded[..]).unwrap();
match &decoded {
TeyrchainBlockData::V0 { block, proof } => {
assert_eq!(v0.header, block[0].header);
assert_eq!(v0.extrinsics, block[0].extrinsics);
assert_eq!(&v0.storage_proof, proof);
},
_ => panic!("Invalid decoding"),
}
let encoded = decoded.as_v0().unwrap().encode();
let decoded = TeyrchainBlockDataV0::<TestBlock>::decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded, v0);
}
#[test]
fn decoding_encoding_v1_works() {
let v1 = TeyrchainBlockData::<TestBlock>::V1 {
blocks: vec![TestBlock::new(
Header::new_from_number(10),
vec![
TestExtrinsic::new_bare(MockCallU64(10)),
TestExtrinsic::new_bare(MockCallU64(100)),
],
)],
proof: CompactProof { encoded_nodes: vec![vec![10u8; 200], vec![20u8; 30]] },
};
let encoded = v1.encode();
let decoded = TeyrchainBlockData::<TestBlock>::decode(&mut &encoded[..]).unwrap();
assert_eq!(v1.blocks(), decoded.blocks());
assert_eq!(v1.proof(), decoded.proof());
}
}
@@ -0,0 +1,32 @@
[package]
name = "pezcumulus-primitives-proof-size-hostfunction"
version = "0.2.0"
authors.workspace = true
edition.workspace = true
description = "Hostfunction exposing storage proof size to the runtime."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
pezsp-externalities = { workspace = true }
pezsp-runtime-interface = { workspace = true }
pezsp-trie = { workspace = true }
[dev-dependencies]
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-state-machine = { workspace = true, default-features = true }
[features]
default = ["std"]
std = ["pezsp-externalities/std", "pezsp-runtime-interface/std", "pezsp-trie/std"]
runtime-benchmarks = [
"pezsp-io/runtime-benchmarks",
"pezsp-runtime-interface/runtime-benchmarks",
"pezsp-state-machine/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
]
@@ -0,0 +1,109 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Tools for reclaiming PoV weight in teyrchain runtimes.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
use pezsp_externalities::ExternalitiesExt;
use pezsp_runtime_interface::runtime_interface;
#[cfg(feature = "std")]
use pezsp_trie::proof_size_extension::ProofSizeExt;
pub const PROOF_RECORDING_DISABLED: u64 = u64::MAX;
/// Interface that provides access to the current storage proof size.
///
/// Should return the current storage proof size if [`ProofSizeExt`] is registered. Otherwise, needs
/// to return u64::MAX.
#[runtime_interface]
pub trait StorageProofSize {
/// Returns the current storage proof size.
fn storage_proof_size(&mut self) -> u64 {
self.extension::<ProofSizeExt>()
.map_or(PROOF_RECORDING_DISABLED, |e| e.storage_proof_size())
}
}
#[cfg(test)]
mod tests {
use pezsp_core::Blake2Hasher;
use pezsp_state_machine::TestExternalities;
use pezsp_trie::{
proof_size_extension::ProofSizeExt, recorder::Recorder, LayoutV1, PrefixedMemoryDB,
TrieDBMutBuilder, TrieMut,
};
use crate::{storage_proof_size, PROOF_RECORDING_DISABLED};
const TEST_DATA: &[(&[u8], &[u8])] = &[(b"key1", &[1; 64]), (b"key2", &[2; 64])];
type TestLayout = LayoutV1<pezsp_core::Blake2Hasher>;
fn get_prepared_test_externalities() -> (TestExternalities<Blake2Hasher>, Recorder<Blake2Hasher>)
{
let mut db = PrefixedMemoryDB::default();
let mut root = Default::default();
{
let mut trie = TrieDBMutBuilder::<TestLayout>::new(&mut db, &mut root).build();
for (k, v) in TEST_DATA {
trie.insert(k, v).expect("Inserts data");
}
}
let recorder: pezsp_trie::recorder::Recorder<Blake2Hasher> = Default::default();
let trie_backend = pezsp_state_machine::TrieBackendBuilder::new(db, root)
.with_recorder(recorder.clone())
.build();
let mut ext: TestExternalities<Blake2Hasher> = TestExternalities::default();
ext.backend = trie_backend;
(ext, recorder)
}
#[test]
fn host_function_returns_size_from_recorder() {
let (mut ext, recorder) = get_prepared_test_externalities();
ext.register_extension(ProofSizeExt::new(recorder));
ext.execute_with(|| {
assert_eq!(storage_proof_size::storage_proof_size(), 0);
pezsp_io::storage::get(b"key1");
assert_eq!(storage_proof_size::storage_proof_size(), 175);
pezsp_io::storage::get(b"key2");
assert_eq!(storage_proof_size::storage_proof_size(), 275);
pezsp_io::storage::get(b"key2");
assert_eq!(storage_proof_size::storage_proof_size(), 275);
});
}
#[test]
fn host_function_returns_max_without_extension() {
let (mut ext, _) = get_prepared_test_externalities();
ext.execute_with(|| {
assert_eq!(storage_proof_size::storage_proof_size(), PROOF_RECORDING_DISABLED);
pezsp_io::storage::get(b"key1");
assert_eq!(storage_proof_size::storage_proof_size(), PROOF_RECORDING_DISABLED);
pezsp_io::storage::get(b"key2");
assert_eq!(storage_proof_size::storage_proof_size(), PROOF_RECORDING_DISABLED);
});
}
}
@@ -0,0 +1,59 @@
[package]
name = "pezcumulus-primitives-storage-weight-reclaim"
version = "1.0.0"
authors.workspace = true
edition.workspace = true
description = "Utilities to reclaim storage weight."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true }
log = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-runtime = { workspace = true }
pezcumulus-primitives-core = { workspace = true }
pezcumulus-primitives-proof-size-hostfunction = { workspace = true }
docify = { workspace = true }
[dev-dependencies]
pezcumulus-test-runtime = { workspace = true }
pezsp-io = { workspace = true }
pezsp-trie = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezcumulus-primitives-core/std",
"pezcumulus-primitives-proof-size-hostfunction/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"scale-info/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-trie/std",
]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezcumulus-primitives-proof-size-hostfunction/runtime-benchmarks",
"pezcumulus-test-runtime/runtime-benchmarks",
"pezframe-benchmarking?/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
]
@@ -0,0 +1,229 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Mechanism to reclaim PoV proof size weight after an extrinsic has been applied.
#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, DecodeWithMemTracking, Encode};
use core::marker::PhantomData;
use cumulus_primitives_core::Weight;
use cumulus_primitives_proof_size_hostfunction::{
storage_proof_size::storage_proof_size, PROOF_RECORDING_DISABLED,
};
use pezframe_support::{
dispatch::{DispatchInfo, PostDispatchInfo},
weights::WeightMeter,
};
use pezframe_system::Config;
use scale_info::TypeInfo;
use pezsp_runtime::{
impl_tx_ext_default,
traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, TransactionExtension},
transaction_validity::TransactionValidityError,
DispatchResult,
};
#[cfg(test)]
mod tests;
const LOG_TARGET: &'static str = "runtime::storage_reclaim";
/// `StorageWeightReclaimer` is a mechanism for manually reclaiming storage weight.
///
/// It internally keeps track of the proof size and storage weight at initialization time. At
/// reclaim it computes the real consumed storage weight and refunds excess weight.
///
/// # Example
#[doc = docify::embed!("src/tests.rs", simple_reclaimer_example)]
pub struct StorageWeightReclaimer {
previous_remaining_proof_size: u64,
previous_reported_proof_size: Option<u64>,
}
impl StorageWeightReclaimer {
/// Creates a new `StorageWeightReclaimer` instance and initializes it with the storage
/// size provided by `weight_meter` and reported proof size from the node.
#[must_use = "Must call `reclaim_with_meter` to reclaim the weight"]
pub fn new(weight_meter: &WeightMeter) -> StorageWeightReclaimer {
let previous_remaining_proof_size = weight_meter.remaining().proof_size();
let previous_reported_proof_size = get_proof_size();
Self { previous_remaining_proof_size, previous_reported_proof_size }
}
/// Check the consumed storage weight and calculate the consumed excess weight.
fn reclaim(&mut self, remaining_weight: Weight) -> Option<Weight> {
let current_remaining_weight = remaining_weight.proof_size();
let current_storage_proof_size = get_proof_size()?;
let previous_storage_proof_size = self.previous_reported_proof_size?;
let used_weight =
self.previous_remaining_proof_size.saturating_sub(current_remaining_weight);
let reported_used_size =
current_storage_proof_size.saturating_sub(previous_storage_proof_size);
let reclaimable = used_weight.saturating_sub(reported_used_size);
log::trace!(
target: LOG_TARGET,
"Found reclaimable storage weight. benchmarked: {used_weight}, consumed: {reported_used_size}"
);
self.previous_remaining_proof_size = current_remaining_weight.saturating_add(reclaimable);
self.previous_reported_proof_size = Some(current_storage_proof_size);
Some(Weight::from_parts(0, reclaimable))
}
/// Check the consumed storage weight and add the reclaimed
/// weight budget back to `weight_meter`.
pub fn reclaim_with_meter(&mut self, weight_meter: &mut WeightMeter) -> Option<Weight> {
let reclaimed = self.reclaim(weight_meter.remaining())?;
weight_meter.reclaim_proof_size(reclaimed.proof_size());
Some(reclaimed)
}
}
/// Returns the current storage proof size from the host side.
///
/// Returns `None` if proof recording is disabled on the host.
pub fn get_proof_size() -> Option<u64> {
let proof_size = storage_proof_size();
(proof_size != PROOF_RECORDING_DISABLED).then_some(proof_size)
}
// Encapsulate into a mod so that macro generated code doesn't trigger a warning about deprecated
// usage.
#[allow(deprecated)]
mod allow_deprecated {
use super::*;
/// Storage weight reclaim mechanism.
///
/// This extension checks the size of the node-side storage proof
/// before and after executing a given extrinsic. The difference between
/// benchmarked and spent weight can be reclaimed.
#[deprecated(note = "This extension doesn't provide accurate reclaim for storage intensive \
transaction extension pipeline; it ignores the validation and preparation of extensions prior \
to itself and ignores the post dispatch logic for extensions subsequent to itself, it also
doesn't provide weight information. \
Use `StorageWeightReclaim` in the `pezcumulus-pezpallet-weight-reclaim` crate")]
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, Default, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct StorageWeightReclaim<T: Config + Send + Sync>(pub(super) PhantomData<T>);
}
#[allow(deprecated)]
pub use allow_deprecated::StorageWeightReclaim;
#[allow(deprecated)]
impl<T: Config + Send + Sync> StorageWeightReclaim<T> {
/// Create a new `StorageWeightReclaim` instance.
pub fn new() -> Self {
Self(Default::default())
}
}
#[allow(deprecated)]
impl<T: Config + Send + Sync> core::fmt::Debug for StorageWeightReclaim<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
let _ = write!(f, "StorageWeightReclaim");
Ok(())
}
}
#[allow(deprecated)]
impl<T: Config + Send + Sync> TransactionExtension<T::RuntimeCall> for StorageWeightReclaim<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
{
const IDENTIFIER: &'static str = "StorageWeightReclaim";
type Implicit = ();
type Val = ();
type Pre = Option<u64>;
fn prepare(
self,
_val: Self::Val,
_origin: &T::RuntimeOrigin,
_call: &T::RuntimeCall,
_info: &DispatchInfoOf<T::RuntimeCall>,
_len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
Ok(get_proof_size())
}
fn post_dispatch_details(
pre: Self::Pre,
info: &DispatchInfoOf<T::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
_len: usize,
_result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
let Some(pre_dispatch_proof_size) = pre else {
return Ok(Weight::zero());
};
let Some(post_dispatch_proof_size) = get_proof_size() else {
log::debug!(
target: LOG_TARGET,
"Proof recording enabled during pre-dispatch, now disabled. This should not happen."
);
return Ok(Weight::zero());
};
// Unspent weight according to the `actual_weight` from `PostDispatchInfo`
// This unspent weight will be refunded by the `CheckWeight` extension, so we need to
// account for that.
let unspent = post_info.calc_unspent(info).proof_size();
let benchmarked_weight = info.total_weight().proof_size().saturating_sub(unspent);
let consumed_weight = post_dispatch_proof_size.saturating_sub(pre_dispatch_proof_size);
let storage_size_diff = benchmarked_weight.abs_diff(consumed_weight as u64);
let extrinsic_len = pezframe_system::AllExtrinsicsLen::<T>::get().unwrap_or(0);
let node_side_pov_size = post_dispatch_proof_size.saturating_add(extrinsic_len.into());
// This value will be reclaimed by [`pezframe_system::CheckWeight`], so we need to calculate
// that in.
pezframe_system::BlockWeight::<T>::mutate(|current| {
if consumed_weight > benchmarked_weight {
log::error!(
target: LOG_TARGET,
"Benchmarked storage weight smaller than consumed storage weight. extrinsic: {} benchmarked: {benchmarked_weight} consumed: {consumed_weight} unspent: {unspent}",
pezframe_system::Pallet::<T>::extrinsic_index().unwrap_or(0)
);
current.accrue(Weight::from_parts(0, storage_size_diff), info.class)
} else {
log::trace!(
target: LOG_TARGET,
"Reclaiming storage weight. extrinsic: {} benchmarked: {benchmarked_weight} consumed: {consumed_weight} unspent: {unspent}",
pezframe_system::Pallet::<T>::extrinsic_index().unwrap_or(0)
);
current.reduce(Weight::from_parts(0, storage_size_diff), info.class)
}
// If we encounter a situation where the node-side proof size is already higher than
// what we have in the runtime bookkeeping, we add the difference to the `BlockWeight`.
// This prevents that the proof size grows faster than the runtime proof size.
let block_weight_proof_size = current.total().proof_size();
let missing_from_node = node_side_pov_size.saturating_sub(block_weight_proof_size);
if missing_from_node > 0 {
log::debug!(
target: LOG_TARGET,
"Node-side PoV size higher than runtime proof size weight. node-side: {node_side_pov_size} extrinsic_len: {extrinsic_len} runtime: {block_weight_proof_size}, missing: {missing_from_node}. Setting to node-side proof size."
);
current.accrue(Weight::from_parts(0, missing_from_node), info.class);
}
});
Ok(Weight::zero())
}
impl_tx_ext_default!(T::RuntimeCall; weight validate);
}
@@ -0,0 +1,721 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use core::marker::PhantomData;
use pezframe_support::{
assert_ok,
dispatch::{DispatchClass, PerDispatchClass},
weights::{Weight, WeightMeter},
};
use pezframe_system::{BlockWeight, CheckWeight};
use pezsp_runtime::{traits::DispatchTransaction, AccountId32, BuildStorage};
use pezsp_trie::proof_size_extension::ProofSizeExt;
type Test = cumulus_test_runtime::Runtime;
const CALL: &<Test as Config>::RuntimeCall =
&cumulus_test_runtime::RuntimeCall::System(pezframe_system::Call::set_heap_pages { pages: 0u64 });
const ALICE: AccountId32 = AccountId32::new([1u8; 32]);
const LEN: usize = 150;
fn new_test_ext() -> pezsp_io::TestExternalities {
let ext: pezsp_io::TestExternalities = cumulus_test_runtime::RuntimeGenesisConfig::default()
.build_storage()
.unwrap()
.into();
ext
}
struct TestRecorder {
return_values: Box<[usize]>,
counter: core::sync::atomic::AtomicUsize,
}
impl TestRecorder {
fn new(values: &[usize]) -> Self {
TestRecorder { return_values: values.into(), counter: Default::default() }
}
}
impl pezsp_trie::ProofSizeProvider for TestRecorder {
fn estimate_encoded_size(&self) -> usize {
let counter = self.counter.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
self.return_values[counter]
}
}
fn setup_test_externalities(proof_values: &[usize]) -> pezsp_io::TestExternalities {
let mut test_ext = new_test_ext();
let test_recorder = TestRecorder::new(proof_values);
test_ext.register_extension(ProofSizeExt::new(test_recorder));
test_ext
}
fn set_current_storage_weight(new_weight: u64) {
BlockWeight::<Test>::mutate(|current_weight| {
current_weight.set(Weight::from_parts(0, new_weight), DispatchClass::Normal);
});
}
fn get_storage_weight() -> PerDispatchClass<Weight> {
BlockWeight::<Test>::get()
}
#[test]
#[allow(deprecated)]
fn basic_refund() {
// The real cost will be 100 bytes of storage size
let mut test_ext = setup_test_externalities(&[0, 100]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 500
let info = DispatchInfo { call_weight: Weight::from_parts(0, 500), ..Default::default() };
let post_info = PostDispatchInfo::default();
// Should add 500 + 150 (len) to weight.
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(0));
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
// We expect a refund of 400
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
assert_eq!(get_storage_weight().total().proof_size(), 1250);
})
}
#[test]
#[allow(deprecated)]
fn underestimating_refund() {
// We fixed a bug where `pre dispatch info weight > consumed weight > post info weight`
// resulted in error.
// The real cost will be 100 bytes of storage size
let mut test_ext = setup_test_externalities(&[0, 100]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 500
let info = DispatchInfo { call_weight: Weight::from_parts(0, 101), ..Default::default() };
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(0, 99)),
pays_fee: Default::default(),
};
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(0));
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(())));
// We expect an accrue of 1
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(())
));
assert_eq!(get_storage_weight().total().proof_size(), 1250);
})
}
#[test]
#[allow(deprecated)]
fn sets_to_node_storage_proof_if_higher() {
// The storage proof reported by the proof recorder is higher than what is stored on
// the runtime side.
{
let mut test_ext = setup_test_externalities(&[1000, 1005]);
test_ext.execute_with(|| {
// Stored in BlockWeight is 5
set_current_storage_weight(5);
// Benchmarked storage weight: 10
let info =
DispatchInfo { call_weight: Weight::from_parts(0, 10), ..Default::default() };
let post_info = PostDispatchInfo::default();
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(1000));
assert_ok!(CheckWeight::<Test>::post_dispatch_details(
(),
&info,
&post_info,
0,
&Ok(())
));
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(())
));
// We expect that the storage weight was set to the node-side proof size (1005) +
// extrinsics length (150)
assert_eq!(get_storage_weight().total().proof_size(), 1155);
})
}
// In this second scenario the proof size on the node side is only lower
// after reclaim happened.
{
let mut test_ext = setup_test_externalities(&[175, 180]);
test_ext.execute_with(|| {
set_current_storage_weight(85);
// Benchmarked storage weight: 100
let info =
DispatchInfo { call_weight: Weight::from_parts(0, 100), ..Default::default() };
let post_info = PostDispatchInfo::default();
// After this pre_dispatch, the BlockWeight proof size will be
// 85 (initial) + 100 (benched) + 150 (tx length) = 335
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(175));
assert_ok!(CheckWeight::<Test>::post_dispatch_details(
(),
&info,
&post_info,
0,
&Ok(())
));
// First we will reclaim 95, which leaves us with 240 BlockWeight. This is lower
// than 180 (proof size hf) + 150 (length), so we expect it to be set to 330.
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(())
));
// We expect that the storage weight was set to the node-side proof weight
assert_eq!(get_storage_weight().total().proof_size(), 330);
})
}
}
#[test]
#[allow(deprecated)]
fn does_nothing_without_extension() {
let mut test_ext = new_test_ext();
// Proof size extension not registered
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 500
let info = DispatchInfo { call_weight: Weight::from_parts(0, 500), ..Default::default() };
let post_info = PostDispatchInfo::default();
// Adds 500 + 150 (len) weight
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, None);
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
assert_eq!(get_storage_weight().total().proof_size(), 1650);
})
}
#[test]
#[allow(deprecated)]
fn negative_refund_is_added_to_weight() {
let mut test_ext = setup_test_externalities(&[100, 300]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 100
let info = DispatchInfo { call_weight: Weight::from_parts(0, 100), ..Default::default() };
let post_info = PostDispatchInfo::default();
// Weight added should be 100 + 150 (len)
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(100));
// We expect no refund
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
assert_eq!(
get_storage_weight().total().proof_size(),
1100 + LEN as u64 + info.total_weight().proof_size()
);
})
}
#[test]
#[allow(deprecated)]
fn test_zero_proof_size() {
let mut test_ext = setup_test_externalities(&[0, 0]);
test_ext.execute_with(|| {
let info = DispatchInfo { call_weight: Weight::from_parts(0, 500), ..Default::default() };
let post_info = PostDispatchInfo::default();
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(0));
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
// Proof size should be exactly equal to extrinsic length
assert_eq!(get_storage_weight().total().proof_size(), LEN as u64);
});
}
#[test]
#[allow(deprecated)]
fn test_larger_pre_dispatch_proof_size() {
let mut test_ext = setup_test_externalities(&[300, 100]);
test_ext.execute_with(|| {
set_current_storage_weight(1300);
let info = DispatchInfo { call_weight: Weight::from_parts(0, 500), ..Default::default() };
let post_info = PostDispatchInfo::default();
// Adds 500 + 150 (len) weight, total weight is 1950
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(300));
// Refund 500 unspent weight according to `post_info`, total weight is now 1650
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
// Recorded proof size is negative -200, total weight is now 1450
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
assert_eq!(get_storage_weight().total().proof_size(), 1450);
});
}
#[test]
#[allow(deprecated)]
fn test_incorporates_check_weight_unspent_weight() {
let mut test_ext = setup_test_externalities(&[100, 300]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 300
let info = DispatchInfo { call_weight: Weight::from_parts(100, 300), ..Default::default() };
// Actual weight is 50
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(50, 250)),
pays_fee: Default::default(),
};
// Should add 300 + 150 (len) of weight
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(100));
// The `CheckWeight` extension will refunt `actual_weight` from `PostDispatchInfo`
// we always need to call `post_dispatch` to verify that they interoperate correctly.
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
// Reclaimed 100
assert_eq!(get_storage_weight().total().proof_size(), 1350);
})
}
#[test]
#[allow(deprecated)]
fn test_incorporates_check_weight_unspent_weight_on_negative() {
let mut test_ext = setup_test_externalities(&[100, 300]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 50
let info = DispatchInfo { call_weight: Weight::from_parts(100, 50), ..Default::default() };
// Actual weight is 25
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(50, 25)),
pays_fee: Default::default(),
};
// Adds 50 + 150 (len) weight, total weight 1200
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(100));
// The `CheckWeight` extension will refunt `actual_weight` from `PostDispatchInfo`
// we always need to call `post_dispatch` to verify that they interoperate correctly.
// Refunds unspent 25 weight according to `post_info`, 1175
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
// Adds 200 - 25 (unspent) == 175 weight, total weight 1350
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
assert_eq!(get_storage_weight().total().proof_size(), 1350);
})
}
#[test]
#[allow(deprecated)]
fn test_nothing_relcaimed() {
let mut test_ext = setup_test_externalities(&[0, 100]);
test_ext.execute_with(|| {
set_current_storage_weight(0);
// Benchmarked storage weight: 100
let info = DispatchInfo { call_weight: Weight::from_parts(100, 100), ..Default::default() };
// Actual proof size is 100
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(50, 100)),
pays_fee: Default::default(),
};
// Adds benchmarked weight 100 + 150 (len), total weight is now 250
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
// Weight should go up by 150 len + 100 proof size weight, total weight 250
assert_eq!(get_storage_weight().total().proof_size(), 250);
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
// Should return `setup_test_externalities` proof recorder value: 100.
assert_eq!(pre, Some(0));
// The `CheckWeight` extension will refund `actual_weight` from `PostDispatchInfo`
// we always need to call `post_dispatch` to verify that they interoperate correctly.
// Nothing to refund, unspent is 0, total weight 250
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, LEN, &Ok(())));
// `setup_test_externalities` proof recorder value: 200, so this means the extrinsic
// actually used 100 proof size.
// Nothing to refund or add, weight matches proof recorder
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(())
));
// Check block len weight was not reclaimed:
// 100 weight + 150 extrinsic len == 250 proof size
assert_eq!(get_storage_weight().total().proof_size(), 250);
})
}
#[test]
#[allow(deprecated)]
fn test_incorporates_check_weight_unspent_weight_reverse_order() {
let mut test_ext = setup_test_externalities(&[100, 300]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 300
let info = DispatchInfo { call_weight: Weight::from_parts(100, 300), ..Default::default() };
// Actual weight is 50
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(50, 250)),
pays_fee: Default::default(),
};
// Adds 300 + 150 (len) weight, total weight 1450
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(100));
// This refunds 100 - 50(unspent), total weight is now 1400
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
// `CheckWeight` gets called after `StorageWeightReclaim` this time.
// The `CheckWeight` extension will refunt `actual_weight` from `PostDispatchInfo`
// we always need to call `post_dispatch` to verify that they interoperate correctly.
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
// Above call refunds 50 (unspent), total weight is 1350 now
assert_eq!(get_storage_weight().total().proof_size(), 1350);
})
}
#[test]
#[allow(deprecated)]
fn test_incorporates_check_weight_unspent_weight_on_negative_reverse_order() {
let mut test_ext = setup_test_externalities(&[100, 300]);
test_ext.execute_with(|| {
set_current_storage_weight(1000);
// Benchmarked storage weight: 50
let info = DispatchInfo { call_weight: Weight::from_parts(100, 50), ..Default::default() };
// Actual weight is 25
let post_info = PostDispatchInfo {
actual_weight: Some(Weight::from_parts(50, 25)),
pays_fee: Default::default(),
};
// Adds 50 + 150 (len) weight, total weight is 1200
let (_, next_len) = CheckWeight::<Test>::do_validate(&info, LEN).unwrap();
assert_ok!(CheckWeight::<Test>::do_prepare(&info, LEN, next_len));
let (pre, _) = StorageWeightReclaim::<Test>(PhantomData)
.validate_and_prepare(Some(ALICE.clone()).into(), CALL, &info, LEN, 0)
.unwrap();
assert_eq!(pre, Some(100));
// Adds additional 150 weight recorded
assert_ok!(StorageWeightReclaim::<Test>::post_dispatch_details(
pre,
&info,
&post_info,
LEN,
&Ok(()),
));
// `CheckWeight` gets called after `StorageWeightReclaim` this time.
// The `CheckWeight` extension will refunt `actual_weight` from `PostDispatchInfo`
// we always need to call `post_dispatch` to verify that they interoperate correctly.
assert_ok!(CheckWeight::<Test>::post_dispatch_details((), &info, &post_info, 0, &Ok(()),));
assert_eq!(get_storage_weight().total().proof_size(), 1350);
})
}
#[test]
fn storage_size_reported_correctly() {
let mut test_ext = setup_test_externalities(&[1000]);
test_ext.execute_with(|| {
assert_eq!(get_proof_size(), Some(1000));
});
let mut test_ext = new_test_ext();
let test_recorder = TestRecorder::new(&[0]);
test_ext.register_extension(ProofSizeExt::new(test_recorder));
test_ext.execute_with(|| {
assert_eq!(get_proof_size(), Some(0));
});
}
#[test]
fn storage_size_disabled_reported_correctly() {
let mut test_ext = setup_test_externalities(&[PROOF_RECORDING_DISABLED as usize]);
test_ext.execute_with(|| {
assert_eq!(get_proof_size(), None);
});
}
#[test]
#[allow(deprecated)]
fn test_reclaim_helper() {
let mut test_ext = setup_test_externalities(&[1000, 1300, 1800]);
test_ext.execute_with(|| {
let mut remaining_weight_meter = WeightMeter::with_limit(Weight::from_parts(0, 2000));
let mut reclaim_helper = StorageWeightReclaimer::new(&remaining_weight_meter);
remaining_weight_meter.consume(Weight::from_parts(0, 500));
let reclaimed = reclaim_helper.reclaim_with_meter(&mut remaining_weight_meter);
assert_eq!(reclaimed, Some(Weight::from_parts(0, 200)));
remaining_weight_meter.consume(Weight::from_parts(0, 800));
let reclaimed = reclaim_helper.reclaim_with_meter(&mut remaining_weight_meter);
assert_eq!(reclaimed, Some(Weight::from_parts(0, 300)));
assert_eq!(remaining_weight_meter.remaining(), Weight::from_parts(0, 1200));
});
}
#[test]
#[allow(deprecated)]
fn test_reclaim_helper_does_not_reclaim_negative() {
// Benchmarked weight does not change at all
let mut test_ext = setup_test_externalities(&[1000, 1300]);
test_ext.execute_with(|| {
let mut remaining_weight_meter = WeightMeter::with_limit(Weight::from_parts(0, 1000));
let mut reclaim_helper = StorageWeightReclaimer::new(&remaining_weight_meter);
let reclaimed = reclaim_helper.reclaim_with_meter(&mut remaining_weight_meter);
assert_eq!(reclaimed, Some(Weight::from_parts(0, 0)));
assert_eq!(remaining_weight_meter.remaining(), Weight::from_parts(0, 1000));
});
// Benchmarked weight increases less than storage proof consumes
let mut test_ext = setup_test_externalities(&[1000, 1300]);
test_ext.execute_with(|| {
let mut remaining_weight_meter = WeightMeter::with_limit(Weight::from_parts(0, 1000));
let mut reclaim_helper = StorageWeightReclaimer::new(&remaining_weight_meter);
remaining_weight_meter.consume(Weight::from_parts(0, 0));
let reclaimed = reclaim_helper.reclaim_with_meter(&mut remaining_weight_meter);
assert_eq!(reclaimed, Some(Weight::from_parts(0, 0)));
});
}
/// Just here for doc purposes
fn get_benched_weight() -> Weight {
Weight::from_parts(0, 5)
}
/// Just here for doc purposes
fn do_work() {}
#[allow(deprecated)]
#[docify::export_content(simple_reclaimer_example)]
fn reclaim_with_weight_meter() {
let mut remaining_weight_meter = WeightMeter::with_limit(Weight::from_parts(10, 10));
let benched_weight = get_benched_weight();
// It is important to instantiate the `StorageWeightReclaimer` before we consume the weight
// for a piece of work from the weight meter.
let mut reclaim_helper = StorageWeightReclaimer::new(&remaining_weight_meter);
if remaining_weight_meter.try_consume(benched_weight).is_ok() {
// Perform some work that takes has `benched_weight` storage weight.
do_work();
// Reclaimer will detect that we only consumed 2 bytes, so 3 bytes are reclaimed.
let reclaimed = reclaim_helper.reclaim_with_meter(&mut remaining_weight_meter);
// We reclaimed 3 bytes of storage size!
assert_eq!(reclaimed, Some(Weight::from_parts(0, 3)));
assert_eq!(get_storage_weight().total().proof_size(), 10);
assert_eq!(remaining_weight_meter.remaining(), Weight::from_parts(10, 8));
}
}
#[test]
fn test_reclaim_helper_works_with_meter() {
// The node will report 12 - 10 = 2 consumed storage size between the calls.
let mut test_ext = setup_test_externalities(&[10, 12]);
test_ext.execute_with(|| {
// Initial storage size is 10.
set_current_storage_weight(10);
reclaim_with_weight_meter();
});
}
@@ -0,0 +1,42 @@
[package]
name = "pezcumulus-primitives-teyrchain-inherent"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Inherent that needs to be present in every teyrchain block. Contains messages and a relay chain storage-proof."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
async-trait = { optional = true, workspace = true }
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
# Bizinikiwi
pezsp-core = { workspace = true }
pezsp-inherents = { workspace = true }
pezsp-trie = { workspace = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true }
[features]
default = ["std"]
std = [
"async-trait",
"codec/std",
"pezcumulus-primitives-core/std",
"scale-info/std",
"pezsp-core/std",
"pezsp-inherents/std",
"pezsp-trie/std",
]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-trie/runtime-benchmarks",
]
@@ -0,0 +1,250 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Pezcumulus teyrchain inherent
//!
//! The [`TeyrchainInherentData`] is the data that is passed by the collator to the teyrchain
//! runtime. The runtime will use this data to execute messages from other teyrchains/the relay
//! chain or to read data from the relay chain state. When the teyrchain is validated by a teyrchain
//! validator on the relay chain, this data is checked for correctness. If the data passed by the
//! collator to the runtime isn't correct, the teyrchain candidate is considered invalid.
//!
//! To create a [`TeyrchainInherentData`] for a specific relay chain block, there exists the
//! `TeyrchainInherentDataExt` trait in `pezcumulus-client-teyrchain-inherent` that helps with this.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{collections::btree_map::BTreeMap, vec::Vec};
use cumulus_primitives_core::{
relay_chain::{
ApprovedPeerId, BlakeTwo256, BlockNumber as RelayChainBlockNumber, Hash as RelayHash,
HashT as _, Header as RelayHeader,
},
InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData,
};
use scale_info::TypeInfo;
use pezsp_inherents::InherentIdentifier;
/// The identifier for the teyrchain inherent.
pub const TEYRCHAIN_INHERENT_IDENTIFIER_V0: InherentIdentifier = *b"sysi1337";
pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"sysi1338";
/// Legacy TeyrchainInherentData that is kept around for backward compatibility.
/// Can be removed once we can safely assume that teyrchain nodes provide the
/// `relay_parent_descendants` and `collator_peer_id` fields.
pub mod v0 {
use alloc::{collections::BTreeMap, vec::Vec};
use cumulus_primitives_core::{
InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData,
};
use scale_info::TypeInfo;
/// The inherent data that is passed by the collator to the teyrchain runtime.
#[derive(
codec::Encode,
codec::Decode,
codec::DecodeWithMemTracking,
pezsp_core::RuntimeDebug,
Clone,
PartialEq,
TypeInfo,
)]
pub struct TeyrchainInherentData {
pub validation_data: PersistedValidationData,
/// A storage proof of a predefined set of keys from the relay-chain.
///
/// Specifically this witness contains the data for:
///
/// - the current slot number at the given relay parent
/// - active host configuration as per the relay parent,
/// - the relay dispatch queue sizes
/// - the list of egress HRMP channels (in the list of recipients form)
/// - the metadata for the egress HRMP channels
pub relay_chain_state: pezsp_trie::StorageProof,
/// Downward messages in the order they were sent.
pub downward_messages: Vec<InboundDownwardMessage>,
/// HRMP messages grouped by channels. The messages in the inner vec must be in order they
/// were sent. In combination with the rule of no more than one message in a channel per
/// block, this means `sent_at` is **strictly** greater than the previous one (if any).
pub horizontal_messages: BTreeMap<ParaId, Vec<InboundHrmpMessage>>,
}
}
/// The inherent data that is passed by the collator to the teyrchain runtime.
#[derive(
codec::Encode,
codec::Decode,
codec::DecodeWithMemTracking,
pezsp_core::RuntimeDebug,
Clone,
PartialEq,
TypeInfo,
)]
pub struct TeyrchainInherentData {
pub validation_data: PersistedValidationData,
/// A storage proof of a predefined set of keys from the relay-chain.
///
/// Specifically this witness contains the data for:
///
/// - the current slot number at the given relay parent
/// - active host configuration as per the relay parent,
/// - the relay dispatch queue sizes
/// - the list of egress HRMP channels (in the list of recipients form)
/// - the metadata for the egress HRMP channels
pub relay_chain_state: pezsp_trie::StorageProof,
/// Downward messages in the order they were sent.
pub downward_messages: Vec<InboundDownwardMessage>,
/// HRMP messages grouped by channels. The messages in the inner vec must be in order they
/// were sent. In combination with the rule of no more than one message in a channel per block,
/// this means `sent_at` is **strictly** greater than the previous one (if any).
pub horizontal_messages: BTreeMap<ParaId, Vec<InboundHrmpMessage>>,
/// Contains the relay parent header and its descendants.
/// This information is used to ensure that a teyrchain node builds blocks
/// at a specified offset from the chain tip rather than directly at the tip.
pub relay_parent_descendants: Vec<RelayHeader>,
/// Contains the collator peer ID, which is later sent by the teyrchain to the
/// relay chain via a UMP signal to promote the reputation of the given peer ID.
pub collator_peer_id: Option<ApprovedPeerId>,
}
// Upgrades the TeyrchainInherentData v0 to the newest format.
impl Into<TeyrchainInherentData> for v0::TeyrchainInherentData {
fn into(self) -> TeyrchainInherentData {
TeyrchainInherentData {
validation_data: self.validation_data,
relay_chain_state: self.relay_chain_state,
downward_messages: self.downward_messages,
horizontal_messages: self.horizontal_messages,
relay_parent_descendants: Vec::new(),
collator_peer_id: None,
}
}
}
#[cfg(feature = "std")]
impl TeyrchainInherentData {
/// Transforms [`TeyrchainInherentData`] into [`v0::TeyrchainInherentData`]. Can be used
/// to create inherent data compatible with old runtimes.
fn as_v0(&self) -> v0::TeyrchainInherentData {
v0::TeyrchainInherentData {
validation_data: self.validation_data.clone(),
relay_chain_state: self.relay_chain_state.clone(),
downward_messages: self.downward_messages.clone(),
horizontal_messages: self.horizontal_messages.clone(),
}
}
}
#[cfg(feature = "std")]
#[async_trait::async_trait]
impl pezsp_inherents::InherentDataProvider for TeyrchainInherentData {
async fn provide_inherent_data(
&self,
inherent_data: &mut pezsp_inherents::InherentData,
) -> Result<(), pezsp_inherents::Error> {
inherent_data.put_data(TEYRCHAIN_INHERENT_IDENTIFIER_V0, &self.as_v0())?;
inherent_data.put_data(INHERENT_IDENTIFIER, &self)
}
async fn try_handle_error(
&self,
_: &pezsp_inherents::InherentIdentifier,
_: &[u8],
) -> Option<Result<(), pezsp_inherents::Error>> {
None
}
}
/// An inbound message whose content was hashed.
#[derive(
codec::Encode,
codec::Decode,
codec::DecodeWithMemTracking,
pezsp_core::RuntimeDebug,
Clone,
PartialEq,
TypeInfo,
)]
pub struct HashedMessage {
pub sent_at: RelayChainBlockNumber,
pub msg_hash: pezsp_core::H256,
}
impl From<&InboundDownwardMessage<RelayChainBlockNumber>> for HashedMessage {
fn from(msg: &InboundDownwardMessage<RelayChainBlockNumber>) -> Self {
Self { sent_at: msg.sent_at, msg_hash: MessageQueueChain::hash_msg_data(&msg.msg) }
}
}
impl From<&InboundHrmpMessage> for HashedMessage {
fn from(msg: &InboundHrmpMessage) -> Self {
Self { sent_at: msg.sent_at, msg_hash: MessageQueueChain::hash_msg_data(&msg.data) }
}
}
/// This struct provides ability to extend a message queue chain (MQC) and compute a new head.
///
/// MQC is an instance of a [hash chain] applied to a message queue. Using a hash chain it's
/// possible to represent a sequence of messages using only a single hash.
///
/// A head for an empty chain is agreed to be a zero hash.
///
/// An instance is used to track either DMP from the relay chain or HRMP across a channel.
/// But a given instance is never used to track both. Therefore, you should call either
/// `extend_downward` or `extend_hrmp`, but not both methods on a single instance.
///
/// [hash chain]: https://en.wikipedia.org/wiki/Hash_chain
#[derive(Default, Clone, codec::Encode, codec::Decode, scale_info::TypeInfo)]
pub struct MessageQueueChain(RelayHash);
impl MessageQueueChain {
/// Create a new instance initialized to `hash`.
pub fn new(hash: RelayHash) -> Self {
Self(hash)
}
/// Hash the provided message data.
fn hash_msg_data(msg: &Vec<u8>) -> pezsp_core::H256 {
BlakeTwo256::hash_of(msg)
}
/// Extend the hash chain with a `HashedMessage`.
pub fn extend_with_hashed_msg(&mut self, hashed_msg: &HashedMessage) -> &mut Self {
let prev_head = self.0;
self.0 = BlakeTwo256::hash_of(&(prev_head, hashed_msg.sent_at, &hashed_msg.msg_hash));
self
}
/// Extend the hash chain with an HRMP message. This method should be used only when
/// this chain is tracking HRMP.
pub fn extend_hrmp(&mut self, horizontal_message: &InboundHrmpMessage) -> &mut Self {
self.extend_with_hashed_msg(&horizontal_message.into())
}
/// Extend the hash chain with a downward message. This method should be used only when
/// this chain is tracking DMP.
pub fn extend_downward(&mut self, downward_message: &InboundDownwardMessage) -> &mut Self {
self.extend_with_hashed_msg(&downward_message.into())
}
/// Return the current head of the message queue chain.
/// This is agreed to be the zero hash for an empty chain.
pub fn head(&self) -> RelayHash {
self.0
}
}
@@ -0,0 +1,29 @@
[package]
name = "pezcumulus-primitives-timestamp"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
description = "Provides timestamp related functionality for teyrchains."
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# Bizinikiwi
pezsp-inherents = { workspace = true }
pezsp-timestamp = { workspace = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true }
[features]
default = ["std"]
std = ["pezcumulus-primitives-core/std", "pezsp-inherents/std", "pezsp-timestamp/std"]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezsp-inherents/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
]
@@ -0,0 +1,70 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Pezcumulus timestamp related primitives.
//!
//! Provides a [`InherentDataProvider`] that should be used in the validation phase of the
//! teyrchain. It will be used to create the inherent data and that will be used to check the
//! inherents inside the teyrchain block (in this case the timestamp inherent). As we don't have
//! access to any clock from the runtime the timestamp is always passed as an inherent into the
//! runtime. To check this inherent when validating the block, we will use the relay chain slot. As
//! the relay chain slot is derived from a timestamp, we can easily convert it back to a timestamp
//! by multiplying it with the slot duration. By comparing the relay chain slot derived timestamp
//! with the timestamp we can ensure that the teyrchain timestamp is reasonable.
#![cfg_attr(not(feature = "std"), no_std)]
use core::time::Duration;
use cumulus_primitives_core::relay_chain::Slot;
use pezsp_inherents::{Error, InherentData};
pub use pezsp_timestamp::{InherentType, INHERENT_IDENTIFIER};
/// The inherent data provider for the timestamp.
///
/// This should be used in the runtime when checking the inherents in the validation phase of the
/// teyrchain.
pub struct InherentDataProvider {
relay_chain_slot: Slot,
relay_chain_slot_duration: Duration,
}
impl InherentDataProvider {
/// Create `Self` from the given relay chain slot and slot duration.
pub fn from_relay_chain_slot_and_duration(
relay_chain_slot: Slot,
relay_chain_slot_duration: Duration,
) -> Self {
Self { relay_chain_slot, relay_chain_slot_duration }
}
/// Create the inherent data.
pub fn create_inherent_data(&self) -> Result<InherentData, Error> {
let mut inherent_data = InherentData::new();
self.provide_inherent_data(&mut inherent_data).map(|_| inherent_data)
}
/// Provide the inherent data into the given `inherent_data`.
pub fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> {
// As the teyrchain starts building at around `relay_chain_slot + 1` we use that slot to
// calculate the timestamp.
let data: InherentType = ((*self.relay_chain_slot + 1) *
self.relay_chain_slot_duration.as_millis() as u64)
.into();
inherent_data.put_data(INHERENT_IDENTIFIER, &data)
}
}
+55
View File
@@ -0,0 +1,55 @@
[package]
name = "pezcumulus-primitives-utility"
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
description = "Helper datatypes for Pezcumulus"
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
codec = { features = ["derive"], workspace = true }
log = { workspace = true }
# Bizinikiwi
pezframe-support = { workspace = true }
pezpallet-asset-conversion = { workspace = true }
pezsp-runtime = { workspace = true }
# Pezkuwi
pezkuwi-runtime-common = { workspace = true }
xcm = { workspace = true }
xcm-builder = { workspace = true }
xcm-executor = { workspace = true }
# Pezcumulus
pezcumulus-primitives-core = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezcumulus-primitives-core/std",
"pezframe-support/std",
"log/std",
"pezpallet-asset-conversion/std",
"pezkuwi-runtime-common/std",
"pezsp-runtime/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"pezcumulus-primitives-core/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezpallet-asset-conversion/runtime-benchmarks",
"pezkuwi-runtime-common/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
+906
View File
@@ -0,0 +1,906 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Helper datatypes for pezcumulus. This includes the [`ParentAsUmp`] routing type which will route
//! messages into an [`UpwardMessageSender`] if the destination is `Parent`.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{vec, vec::Vec};
use codec::Encode;
use core::marker::PhantomData;
use cumulus_primitives_core::{MessageSendError, UpwardMessageSender};
use pezframe_support::{
defensive,
traits::{tokens::fungibles, Get, OnUnbalanced as OnUnbalancedT},
weights::{Weight, WeightToFee as WeightToFeeT},
CloneNoBound,
};
use pezpallet_asset_conversion::SwapCredit as SwapCreditT;
use pezkuwi_runtime_common::xcm_sender::PriceForMessageDelivery;
use pezsp_runtime::{
traits::{Saturating, Zero},
SaturatedConversion,
};
use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion};
use xcm_builder::{InspectMessageQueues, TakeRevenue};
use xcm_executor::{
traits::{MatchesFungibles, TransactAsset, WeightTrader},
AssetsInHolding,
};
#[cfg(test)]
mod tests;
/// Xcm router which recognises the `Parent` destination and handles it by sending the message into
/// the given UMP `UpwardMessageSender` implementation. Thus this essentially adapts an
/// `UpwardMessageSender` trait impl into a `SendXcm` trait impl.
///
/// NOTE: This is a pretty dumb "just send it" router; we will probably want to introduce queuing
/// to UMP eventually and when we do, the pallet which implements the queuing will be responsible
/// for the `SendXcm` implementation.
pub struct ParentAsUmp<T, W, P>(PhantomData<(T, W, P)>);
impl<T, W, P> SendXcm for ParentAsUmp<T, W, P>
where
T: UpwardMessageSender,
W: WrapVersion,
P: PriceForMessageDelivery<Id = ()>,
{
type Ticket = Vec<u8>;
fn validate(dest: &mut Option<Location>, msg: &mut Option<Xcm<()>>) -> SendResult<Vec<u8>> {
let d = dest.take().ok_or(SendError::MissingArgument)?;
if d.contains_parents_only(1) {
// An upward message for the relay chain.
let xcm = msg.take().ok_or(SendError::MissingArgument)?;
let price = P::price_for_delivery((), &xcm);
let versioned_xcm =
W::wrap_version(&d, xcm).map_err(|()| SendError::DestinationUnsupported)?;
versioned_xcm
.check_is_decodable()
.map_err(|()| SendError::ExceedsMaxMessageSize)?;
let data = versioned_xcm.encode();
// Pre-check with our message sender if everything else is okay.
T::can_send_upward_message(&data).map_err(Self::map_upward_sender_err)?;
Ok((data, price))
} else {
// Anything else is unhandled. This includes a message that is not meant for us.
// We need to make sure that dest/msg is not consumed here.
*dest = Some(d);
Err(SendError::NotApplicable)
}
}
fn deliver(data: Vec<u8>) -> Result<XcmHash, SendError> {
let (_, hash) = T::send_upward_message(data).map_err(Self::map_upward_sender_err)?;
Ok(hash)
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful_delivery(location: Option<Location>) {
if location.as_ref().map_or(false, |l| l.contains_parents_only(1)) {
T::ensure_successful_delivery();
}
}
}
impl<T, W, P> ParentAsUmp<T, W, P> {
fn map_upward_sender_err(message_send_error: MessageSendError) -> SendError {
match message_send_error {
MessageSendError::TooBig => SendError::ExceedsMaxMessageSize,
e => SendError::Transport(e.into()),
}
}
}
impl<T: UpwardMessageSender + InspectMessageQueues, W, P> InspectMessageQueues
for ParentAsUmp<T, W, P>
{
fn clear_messages() {
T::clear_messages();
}
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
T::get_messages()
}
}
/// Contains information to handle refund/payment for xcm-execution
#[derive(Clone, Eq, PartialEq, Debug)]
struct AssetTraderRefunder {
// The amount of weight bought minus the weigh already refunded
weight_outstanding: Weight,
// The concrete asset containing the asset location and outstanding balance
outstanding_concrete_asset: Asset,
}
/// Charges for execution in the first asset of those selected for fee payment
/// Only succeeds for Concrete Fungible Assets
/// First tries to convert the this Asset into a local assetId
/// Then charges for this assetId as described by FeeCharger
/// Weight, paid balance, local asset Id and the location is stored for
/// later refund purposes
/// Important: Errors if the Trader is being called twice by 2 BuyExecution instructions
/// Alternatively we could just return payment in the aforementioned case
#[derive(CloneNoBound)]
pub struct TakeFirstAssetTrader<
AccountId: Eq,
FeeCharger: ChargeWeightInFungibles<AccountId, ConcreteAssets>,
Matcher: MatchesFungibles<ConcreteAssets::AssetId, ConcreteAssets::Balance>,
ConcreteAssets: fungibles::Mutate<AccountId> + fungibles::Balanced<AccountId>,
HandleRefund: TakeRevenue,
>(
Option<AssetTraderRefunder>,
PhantomData<(AccountId, FeeCharger, Matcher, ConcreteAssets, HandleRefund)>,
);
impl<
AccountId: Eq,
FeeCharger: ChargeWeightInFungibles<AccountId, ConcreteAssets>,
Matcher: MatchesFungibles<ConcreteAssets::AssetId, ConcreteAssets::Balance>,
ConcreteAssets: fungibles::Mutate<AccountId> + fungibles::Balanced<AccountId>,
HandleRefund: TakeRevenue,
> WeightTrader
for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, ConcreteAssets, HandleRefund>
{
fn new() -> Self {
Self(None, PhantomData)
}
// We take first asset
// Check whether we can convert fee to asset_fee (is_sufficient, min_deposit)
// If everything goes well, we charge.
fn buy_weight(
&mut self,
weight: Weight,
payment: xcm_executor::AssetsInHolding,
context: &XcmContext,
) -> Result<xcm_executor::AssetsInHolding, XcmError> {
log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}, context: {:?}", weight, payment, context);
// Make sure we don't enter twice
if self.0.is_some() {
return Err(XcmError::NotWithdrawable);
}
// We take the very first asset from payment
// (assets are sorted by fungibility/amount after this conversion)
let assets: Assets = payment.clone().into();
// Take the first asset from the selected Assets
let first = assets.get(0).ok_or(XcmError::AssetNotFound)?;
// Get the local asset id in which we can pay for fees
let (local_asset_id, _) =
Matcher::matches_fungibles(first).map_err(|_| XcmError::AssetNotFound)?;
// Calculate how much we should charge in the asset_id for such amount of weight
// Require at least a payment of minimum_balance
// Necessary for fully collateral-backed assets
let asset_balance: u128 =
FeeCharger::charge_weight_in_fungibles(local_asset_id.clone(), weight)
.map(|amount| {
let minimum_balance = ConcreteAssets::minimum_balance(local_asset_id);
if amount < minimum_balance {
minimum_balance
} else {
amount
}
})?
.try_into()
.map_err(|_| XcmError::Overflow)?;
// Convert to the same kind of asset, with the required fungible balance
let required = first.id.clone().into_asset(asset_balance.into());
// Subtract payment
let unused = payment.checked_sub(required.clone()).map_err(|_| XcmError::TooExpensive)?;
// record weight and asset
self.0 = Some(AssetTraderRefunder {
weight_outstanding: weight,
outstanding_concrete_asset: required,
});
Ok(unused)
}
fn refund_weight(&mut self, weight: Weight, context: &XcmContext) -> Option<Asset> {
log::trace!(target: "xcm::weight", "TakeFirstAssetTrader::refund_weight weight: {:?}, context: {:?}", weight, context);
if let Some(AssetTraderRefunder {
mut weight_outstanding,
outstanding_concrete_asset: Asset { id, fun },
}) = self.0.clone()
{
// Get the local asset id in which we can refund fees
let (local_asset_id, outstanding_balance) =
Matcher::matches_fungibles(&(id.clone(), fun).into()).ok()?;
let minimum_balance = ConcreteAssets::minimum_balance(local_asset_id.clone());
// Calculate asset_balance
// This read should have already be cached in buy_weight
let (asset_balance, outstanding_minus_subtracted) =
FeeCharger::charge_weight_in_fungibles(local_asset_id, weight).ok().map(
|asset_balance| {
// Require at least a drop of minimum_balance
// Necessary for fully collateral-backed assets
if outstanding_balance.saturating_sub(asset_balance) > minimum_balance {
(asset_balance, outstanding_balance.saturating_sub(asset_balance))
}
// If the amount to be refunded leaves the remaining balance below ED,
// we just refund the exact amount that guarantees at least ED will be
// dropped
else {
(outstanding_balance.saturating_sub(minimum_balance), minimum_balance)
}
},
)?;
// Convert balances into u128
let outstanding_minus_subtracted: u128 = outstanding_minus_subtracted.saturated_into();
let asset_balance: u128 = asset_balance.saturated_into();
// Construct outstanding_concrete_asset with the same location id and subtracted
// balance
let outstanding_concrete_asset: Asset =
(id.clone(), outstanding_minus_subtracted).into();
// Subtract from existing weight and balance
weight_outstanding = weight_outstanding.saturating_sub(weight);
// Override AssetTraderRefunder
self.0 = Some(AssetTraderRefunder { weight_outstanding, outstanding_concrete_asset });
// Only refund if positive
if asset_balance > 0 {
Some((id, asset_balance).into())
} else {
None
}
} else {
None
}
}
}
impl<
AccountId: Eq,
FeeCharger: ChargeWeightInFungibles<AccountId, ConcreteAssets>,
Matcher: MatchesFungibles<ConcreteAssets::AssetId, ConcreteAssets::Balance>,
ConcreteAssets: fungibles::Mutate<AccountId> + fungibles::Balanced<AccountId>,
HandleRefund: TakeRevenue,
> Drop for TakeFirstAssetTrader<AccountId, FeeCharger, Matcher, ConcreteAssets, HandleRefund>
{
fn drop(&mut self) {
if let Some(asset_trader) = self.0.clone() {
HandleRefund::take_revenue(asset_trader.outstanding_concrete_asset);
}
}
}
/// XCM fee depositor to which we implement the `TakeRevenue` trait.
/// It receives a `Transact` implemented argument and a 32 byte convertible `AccountId`, and the fee
/// receiver account's `FungiblesMutateAdapter` should be identical to that implemented by
/// `WithdrawAsset`.
pub struct XcmFeesTo32ByteAccount<FungiblesMutateAdapter, AccountId, ReceiverAccount>(
PhantomData<(FungiblesMutateAdapter, AccountId, ReceiverAccount)>,
);
impl<
FungiblesMutateAdapter: TransactAsset,
AccountId: Clone + Into<[u8; 32]>,
ReceiverAccount: Get<Option<AccountId>>,
> TakeRevenue for XcmFeesTo32ByteAccount<FungiblesMutateAdapter, AccountId, ReceiverAccount>
{
fn take_revenue(revenue: Asset) {
if let Some(receiver) = ReceiverAccount::get() {
let ok = FungiblesMutateAdapter::deposit_asset(
&revenue,
&([AccountId32 { network: None, id: receiver.into() }].into()),
None,
)
.is_ok();
debug_assert!(ok, "`deposit_asset` cannot generally fail; qed");
}
}
}
/// ChargeWeightInFungibles trait, which converts a given amount of weight
/// and an assetId, and it returns the balance amount that should be charged
/// in such assetId for that amount of weight
pub trait ChargeWeightInFungibles<AccountId, Assets: fungibles::Inspect<AccountId>> {
fn charge_weight_in_fungibles(
asset_id: <Assets as fungibles::Inspect<AccountId>>::AssetId,
weight: Weight,
) -> Result<<Assets as fungibles::Inspect<AccountId>>::Balance, XcmError>;
}
/// Provides an implementation of [`WeightTrader`] to charge for weight using the first asset
/// specified in the `payment` argument.
///
/// The asset used to pay for the weight must differ from the `Target` asset and be exchangeable for
/// the same `Target` asset through `SwapCredit`.
///
/// ### Parameters:
/// - `Target`: the asset into which the user's payment will be exchanged using `SwapCredit`.
/// - `SwapCredit`: mechanism used for the exchange of the user's payment asset into the `Target`.
/// - `WeightToFee`: weight to the `Target` asset fee calculator.
/// - `Fungibles`: registry of fungible assets.
/// - `FungiblesAssetMatcher`: utility for mapping [`Asset`] to `Fungibles::AssetId` and
/// `Fungibles::Balance`.
/// - `OnUnbalanced`: handler for the fee payment.
/// - `AccountId`: the account identifier type.
pub struct SwapFirstAssetTrader<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> where
Fungibles::Balance: Into<u128>,
{
/// Accumulated fee paid for XCM execution.
total_fee: fungibles::Credit<AccountId, Fungibles>,
/// Last asset utilized by a client to settle a fee.
last_fee_asset: Option<AssetId>,
_phantom_data: PhantomData<(
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
)>,
}
impl<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> WeightTrader
for SwapFirstAssetTrader<
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
>
where
Fungibles::Balance: Into<u128>,
{
fn new() -> Self {
Self {
total_fee: fungibles::Credit::<AccountId, Fungibles>::zero(Target::get()),
last_fee_asset: None,
_phantom_data: PhantomData,
}
}
fn buy_weight(
&mut self,
weight: Weight,
mut payment: AssetsInHolding,
_context: &XcmContext,
) -> Result<AssetsInHolding, XcmError> {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::buy_weight weight: {:?}, payment: {:?}",
weight,
payment,
);
let first_asset: Asset =
payment.fungible.pop_first().ok_or(XcmError::AssetNotFound)?.into();
let (fungibles_asset, balance) = FungiblesAssetMatcher::matches_fungibles(&first_asset)
.map_err(|error| {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::buy_weight asset {:?} didn't match. Error: {:?}",
first_asset,
error,
);
XcmError::AssetNotFound
})?;
let swap_asset = fungibles_asset.clone().into();
if Target::get().eq(&swap_asset) {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::buy_weight Asset was same as Target, swap not needed.",
);
// current trader is not applicable.
return Err(XcmError::FeesNotMet);
}
let credit_in = Fungibles::issue(fungibles_asset, balance);
let fee = WeightToFee::weight_to_fee(&weight);
// swap the user's asset for the `Target` asset.
let (credit_out, credit_change) = SwapCredit::swap_tokens_for_exact_tokens(
vec![swap_asset, Target::get()],
credit_in,
fee,
)
.map_err(|(credit_in, error)| {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::buy_weight swap couldn't be done. Error was: {:?}",
error,
);
drop(credit_in);
XcmError::FeesNotMet
})?;
match self.total_fee.subsume(credit_out) {
Err(credit_out) => {
// error may occur if `total_fee.asset` differs from `credit_out.asset`, which does
// not apply in this context.
defensive!(
"`total_fee.asset` must be equal to `credit_out.asset`",
(self.total_fee.asset(), credit_out.asset())
);
return Err(XcmError::FeesNotMet);
},
_ => (),
};
self.last_fee_asset = Some(first_asset.id.clone());
payment.fungible.insert(first_asset.id, credit_change.peek().into());
drop(credit_change);
Ok(payment)
}
fn refund_weight(&mut self, weight: Weight, _context: &XcmContext) -> Option<Asset> {
log::trace!(
target: "xcm::weight",
"SwapFirstAssetTrader::refund_weight weight: {:?}, self.total_fee: {:?}",
weight,
self.total_fee,
);
if self.total_fee.peek().is_zero() {
// noting yet paid to refund.
return None;
}
let mut refund_asset = if let Some(asset) = &self.last_fee_asset {
// create an initial zero refund in the asset used in the last `buy_weight`.
(asset.clone(), Fungible(0)).into()
} else {
return None;
};
let refund_amount = WeightToFee::weight_to_fee(&weight);
if refund_amount >= self.total_fee.peek() {
// not enough was paid to refund the `weight`.
return None;
}
let refund_swap_asset = FungiblesAssetMatcher::matches_fungibles(&refund_asset)
.map(|(a, _)| a.into())
.ok()?;
let refund = self.total_fee.extract(refund_amount);
let refund = match SwapCredit::swap_exact_tokens_for_tokens(
vec![Target::get(), refund_swap_asset],
refund,
None,
) {
Ok(refund_in_target) => refund_in_target,
Err((refund, _)) => {
// return an attempted refund back to the `total_fee`.
let _ = self.total_fee.subsume(refund).map_err(|refund| {
// error may occur if `total_fee.asset` differs from `refund.asset`, which does
// not apply in this context.
defensive!(
"`total_fee.asset` must be equal to `refund.asset`",
(self.total_fee.asset(), refund.asset())
);
});
return None;
},
};
refund_asset.fun = refund.peek().into().into();
drop(refund);
Some(refund_asset)
}
}
impl<
Target: Get<Fungibles::AssetId>,
SwapCredit: SwapCreditT<
AccountId,
Balance = Fungibles::Balance,
AssetKind = Fungibles::AssetId,
Credit = fungibles::Credit<AccountId, Fungibles>,
>,
WeightToFee: WeightToFeeT<Balance = Fungibles::Balance>,
Fungibles: fungibles::Balanced<AccountId>,
FungiblesAssetMatcher: MatchesFungibles<Fungibles::AssetId, Fungibles::Balance>,
OnUnbalanced: OnUnbalancedT<fungibles::Credit<AccountId, Fungibles>>,
AccountId,
> Drop
for SwapFirstAssetTrader<
Target,
SwapCredit,
WeightToFee,
Fungibles,
FungiblesAssetMatcher,
OnUnbalanced,
AccountId,
>
where
Fungibles::Balance: Into<u128>,
{
fn drop(&mut self) {
if self.total_fee.peek().is_zero() {
return;
}
let total_fee = self.total_fee.extract(self.total_fee.peek());
OnUnbalanced::on_unbalanced(total_fee);
}
}
#[cfg(test)]
mod test_xcm_router {
use super::*;
use cumulus_primitives_core::UpwardMessage;
use pezframe_support::assert_ok;
use xcm::MAX_XCM_DECODE_DEPTH;
/// Validates [`validate`] for required Some(destination) and Some(message)
struct OkFixedXcmHashWithAssertingRequiredInputsSender;
impl OkFixedXcmHashWithAssertingRequiredInputsSender {
const FIXED_XCM_HASH: [u8; 32] = [9; 32];
fn fixed_delivery_asset() -> Assets {
Assets::new()
}
fn expected_delivery_result() -> Result<(XcmHash, Assets), SendError> {
Ok((Self::FIXED_XCM_HASH, Self::fixed_delivery_asset()))
}
}
impl SendXcm for OkFixedXcmHashWithAssertingRequiredInputsSender {
type Ticket = ();
fn validate(
destination: &mut Option<Location>,
message: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
assert!(destination.is_some());
assert!(message.is_some());
Ok(((), OkFixedXcmHashWithAssertingRequiredInputsSender::fixed_delivery_asset()))
}
fn deliver(_: Self::Ticket) -> Result<XcmHash, SendError> {
Ok(Self::FIXED_XCM_HASH)
}
}
/// Impl [`UpwardMessageSender`] that return `Ok` for `can_send_upward_message`.
struct CanSendUpwardMessageSender;
impl UpwardMessageSender for CanSendUpwardMessageSender {
fn send_upward_message(_: UpwardMessage) -> Result<(u32, XcmHash), MessageSendError> {
Err(MessageSendError::Other)
}
fn can_send_upward_message(_: &UpwardMessage) -> Result<(), MessageSendError> {
Ok(())
}
}
#[test]
fn parent_as_ump_does_not_consume_dest_or_msg_on_not_applicable() {
// dummy message
let message = Xcm(vec![Trap(5)]);
// ParentAsUmp - check dest is really not applicable
let dest = (Parent, Parent, Parent);
let mut dest_wrapper = Some(dest.into());
let mut msg_wrapper = Some(message.clone());
assert_eq!(
Err(SendError::NotApplicable),
<ParentAsUmp<(), (), ()> as SendXcm>::validate(&mut dest_wrapper, &mut msg_wrapper)
);
// check wrapper were not consumed
assert_eq!(Some(dest.into()), dest_wrapper.take());
assert_eq!(Some(message.clone()), msg_wrapper.take());
// another try with router chain with asserting sender
assert_eq!(
OkFixedXcmHashWithAssertingRequiredInputsSender::expected_delivery_result(),
send_xcm::<(ParentAsUmp<(), (), ()>, OkFixedXcmHashWithAssertingRequiredInputsSender)>(
dest.into(),
message
)
);
}
#[test]
fn parent_as_ump_consumes_dest_and_msg_on_ok_validate() {
// dummy message
let message = Xcm(vec![Trap(5)]);
// ParentAsUmp - check dest/msg is valid
let dest = (Parent, Here);
let mut dest_wrapper = Some(dest.clone().into());
let mut msg_wrapper = Some(message.clone());
assert!(<ParentAsUmp<CanSendUpwardMessageSender, (), ()> as SendXcm>::validate(
&mut dest_wrapper,
&mut msg_wrapper
)
.is_ok());
// check wrapper were consumed
assert_eq!(None, dest_wrapper.take());
assert_eq!(None, msg_wrapper.take());
// another try with router chain with asserting sender
assert_eq!(
Err(SendError::Transport("Other")),
send_xcm::<(
ParentAsUmp<CanSendUpwardMessageSender, (), ()>,
OkFixedXcmHashWithAssertingRequiredInputsSender
)>(dest.into(), message)
);
}
#[test]
fn parent_as_ump_validate_nested_xcm_works() {
let dest = Parent;
type Router = ParentAsUmp<CanSendUpwardMessageSender, (), ()>;
// Message that is not too deeply nested:
let mut good = Xcm(vec![ClearOrigin]);
for _ in 0..MAX_XCM_DECODE_DEPTH - 1 {
good = Xcm(vec![SetAppendix(good)]);
}
// Check that the good message is validated:
assert_ok!(<Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(good.clone())));
// Nesting the message one more time should reject it:
let bad = Xcm(vec![SetAppendix(good)]);
assert_eq!(
Err(SendError::ExceedsMaxMessageSize),
<Router as SendXcm>::validate(&mut Some(dest.into()), &mut Some(bad))
);
}
}
#[cfg(test)]
mod test_trader {
use super::*;
use pezframe_support::{
assert_ok,
traits::tokens::{
DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
},
};
use pezsp_runtime::DispatchError;
use xcm_executor::{traits::Error, AssetsInHolding};
#[test]
fn take_first_asset_trader_buy_weight_called_twice_throws_error() {
const AMOUNT: u128 = 100;
// prepare prerequisites to instantiate `TakeFirstAssetTrader`
type TestAccountId = u32;
type TestAssetId = u32;
type TestBalance = u128;
struct TestAssets;
impl MatchesFungibles<TestAssetId, TestBalance> for TestAssets {
fn matches_fungibles(a: &Asset) -> Result<(TestAssetId, TestBalance), Error> {
match a {
Asset { fun: Fungible(amount), id: AssetId(_id) } => Ok((1, *amount)),
_ => Err(Error::AssetNotHandled),
}
}
}
impl fungibles::Inspect<TestAccountId> for TestAssets {
type AssetId = TestAssetId;
type Balance = TestBalance;
fn total_issuance(_: Self::AssetId) -> Self::Balance {
todo!()
}
fn minimum_balance(_: Self::AssetId) -> Self::Balance {
0
}
fn balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
todo!()
}
fn total_balance(_: Self::AssetId, _: &TestAccountId) -> Self::Balance {
todo!()
}
fn reducible_balance(
_: Self::AssetId,
_: &TestAccountId,
_: Preservation,
_: Fortitude,
) -> Self::Balance {
todo!()
}
fn can_deposit(
_: Self::AssetId,
_: &TestAccountId,
_: Self::Balance,
_: Provenance,
) -> DepositConsequence {
todo!()
}
fn can_withdraw(
_: Self::AssetId,
_: &TestAccountId,
_: Self::Balance,
) -> WithdrawConsequence<Self::Balance> {
todo!()
}
fn asset_exists(_: Self::AssetId) -> bool {
todo!()
}
}
impl fungibles::Mutate<TestAccountId> for TestAssets {}
impl fungibles::Balanced<TestAccountId> for TestAssets {
type OnDropCredit = fungibles::DecreaseIssuance<TestAccountId, Self>;
type OnDropDebt = fungibles::IncreaseIssuance<TestAccountId, Self>;
}
impl fungibles::Unbalanced<TestAccountId> for TestAssets {
fn handle_dust(_: fungibles::Dust<TestAccountId, Self>) {
todo!()
}
fn write_balance(
_: Self::AssetId,
_: &TestAccountId,
_: Self::Balance,
) -> Result<Option<Self::Balance>, DispatchError> {
todo!()
}
fn set_total_issuance(_: Self::AssetId, _: Self::Balance) {
todo!()
}
}
struct FeeChargerAssetsHandleRefund;
impl ChargeWeightInFungibles<TestAccountId, TestAssets> for FeeChargerAssetsHandleRefund {
fn charge_weight_in_fungibles(
_: <TestAssets as fungibles::Inspect<TestAccountId>>::AssetId,
_: Weight,
) -> Result<<TestAssets as fungibles::Inspect<TestAccountId>>::Balance, XcmError> {
Ok(AMOUNT)
}
}
impl TakeRevenue for FeeChargerAssetsHandleRefund {
fn take_revenue(_: Asset) {}
}
// create new instance
type Trader = TakeFirstAssetTrader<
TestAccountId,
FeeChargerAssetsHandleRefund,
TestAssets,
TestAssets,
FeeChargerAssetsHandleRefund,
>;
let mut trader = <Trader as WeightTrader>::new();
let ctx = XcmContext { origin: None, message_id: XcmHash::default(), topic: None };
// prepare test data
let asset: Asset = (Here, AMOUNT).into();
let payment = AssetsInHolding::from(asset);
let weight_to_buy = Weight::from_parts(1_000, 1_000);
// lets do first call (success)
assert_ok!(trader.buy_weight(weight_to_buy, payment.clone(), &ctx));
// lets do second call (error)
assert_eq!(trader.buy_weight(weight_to_buy, payment, &ctx), Err(XcmError::NotWithdrawable));
}
}
/// Implementation of `xcm_builder::EnsureDelivery` which helps to ensure delivery to the
/// parent relay chain. Deposits existential deposit for origin (if needed).
/// Deposits estimated fee to the origin account (if needed).
/// Allows triggering of additional logic for a specific `ParaId` (e.g. to open an HRMP channel) if
/// needed.
#[cfg(feature = "runtime-benchmarks")]
pub struct ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>(
core::marker::PhantomData<(XcmConfig, ExistentialDeposit, PriceForDelivery)>,
);
#[cfg(feature = "runtime-benchmarks")]
impl<
XcmConfig: xcm_executor::Config,
ExistentialDeposit: Get<Option<Asset>>,
PriceForDelivery: PriceForMessageDelivery<Id = ()>,
> xcm_builder::EnsureDelivery
for ToParentDeliveryHelper<XcmConfig, ExistentialDeposit, PriceForDelivery>
{
fn ensure_successful_delivery(
origin_ref: &Location,
dest: &Location,
fee_reason: xcm_executor::traits::FeeReason,
) -> (Option<xcm_executor::FeesMode>, Option<Assets>) {
use xcm::{latest::MAX_ITEMS_IN_ASSETS, MAX_INSTRUCTIONS_TO_DECODE};
use xcm_executor::{traits::FeeManager, FeesMode};
// check if the destination is relay/parent
if dest.ne(&Location::parent()) {
return (None, None);
}
// Ensure routers
XcmConfig::XcmSender::ensure_successful_delivery(Some(Location::parent()));
let mut fees_mode = None;
if !XcmConfig::FeeManager::is_waived(Some(origin_ref), fee_reason) {
// if not waived, we need to set up accounts for paying and receiving fees
// mint ED to origin if needed
if let Some(ed) = ExistentialDeposit::get() {
XcmConfig::AssetTransactor::deposit_asset(&ed, &origin_ref, None).unwrap();
}
// overestimate delivery fee
let mut max_assets: Vec<Asset> = Vec::new();
for i in 0..MAX_ITEMS_IN_ASSETS {
max_assets.push((GeneralIndex(i as u128), 100u128).into());
}
let overestimated_xcm =
vec![WithdrawAsset(max_assets.into()); MAX_INSTRUCTIONS_TO_DECODE as usize].into();
let overestimated_fees = PriceForDelivery::price_for_delivery((), &overestimated_xcm);
// mint overestimated fee to origin
for fee in overestimated_fees.inner() {
XcmConfig::AssetTransactor::deposit_asset(&fee, &origin_ref, None).unwrap();
}
// expected worst case - direct withdraw
fees_mode = Some(FeesMode { jit_withdraw: true });
}
(fees_mode, None)
}
}
@@ -0,0 +1,17 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod swap_first;
@@ -0,0 +1,551 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezcumulus.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::*;
use pezframe_support::{parameter_types, traits::fungibles::Inspect};
use mock::{setup_pool, AccountId, AssetId, Balance, Fungibles};
use xcm::latest::AssetId as XcmAssetId;
use xcm_executor::AssetsInHolding;
fn create_holding_asset(asset_id: AssetId, amount: Balance) -> AssetsInHolding {
create_asset(asset_id, amount).into()
}
fn create_asset(asset_id: AssetId, amount: Balance) -> Asset {
Asset { id: create_asset_id(asset_id), fun: Fungible(amount) }
}
fn create_asset_id(asset_id: AssetId) -> XcmAssetId {
AssetId(Location::new(0, [GeneralIndex(asset_id.into())]))
}
fn xcm_context() -> XcmContext {
XcmContext { origin: None, message_id: [0u8; 32], topic: None }
}
fn weight_worth_of(fee: Balance) -> Weight {
Weight::from_parts(fee.try_into().unwrap(), 0)
}
const TARGET_ASSET: AssetId = 1;
const CLIENT_ASSET: AssetId = 2;
const CLIENT_ASSET_2: AssetId = 3;
parameter_types! {
pub const TargetAsset: AssetId = TARGET_ASSET;
}
pub type Trader = SwapFirstAssetTrader<
TargetAsset,
mock::Swap,
mock::WeightToFee,
mock::Fungibles,
mock::FungiblesMatcher,
(),
AccountId,
>;
#[test]
fn holding_asset_swap_for_target() {
let client_asset_total = 15;
let fee = 5;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn holding_asset_swap_for_target_twice() {
let client_asset_total = 20;
let fee1 = 5;
let fee2 = 6;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change1 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
let holding_change2 = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1 - fee2);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
holding_change1
);
assert_eq!(
trader
.buy_weight(weight_worth_of(fee2), holding_change1, &xcm_context())
.unwrap(),
holding_change2
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1 + fee2);
}
#[test]
fn buy_and_refund_twice_for_target() {
let client_asset_total = 15;
let fee = 5;
let refund1 = 4;
let refund2 = 2;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
// create pool for refund swap.
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let refund_asset = create_asset(CLIENT_ASSET, refund1);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
assert_eq!(trader.total_fee.peek(), fee - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund2), &xcm_context()), None);
assert_eq!(trader.total_fee.peek(), fee - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee - refund1);
}
#[test]
fn buy_with_various_assets_and_refund_for_target() {
let client_asset_total = 10;
let client_asset_2_total = 15;
let fee1 = 5;
let fee2 = 6;
let refund1 = 6;
let refund2 = 4;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
setup_pool(CLIENT_ASSET_2, 1000, TARGET_ASSET, 1000);
// create pool for refund swap.
setup_pool(TARGET_ASSET, 1000, CLIENT_ASSET_2, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_asset_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee1);
let holding_change_2 = create_holding_asset(CLIENT_ASSET_2, client_asset_2_total - fee2);
// both refunds in the latest buy asset (`CLIENT_ASSET_2`).
let refund_asset = create_asset(CLIENT_ASSET_2, refund1);
let refund_asset_2 = create_asset(CLIENT_ASSET_2, refund2);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let client_total_2 = Fungibles::total_issuance(CLIENT_ASSET_2);
let mut trader = Trader::new();
// first purchase with `CLIENT_ASSET`.
assert_eq!(
trader.buy_weight(weight_worth_of(fee1), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
// second purchase with `CLIENT_ASSET_2`.
assert_eq!(
trader
.buy_weight(weight_worth_of(fee2), holding_asset_2, &xcm_context())
.unwrap(),
holding_change_2
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
// first refund in the last asset used with `buy_weight`.
assert_eq!(trader.refund_weight(weight_worth_of(refund1), &xcm_context()), Some(refund_asset));
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
// second refund in the last asset used with `buy_weight`.
assert_eq!(
trader.refund_weight(weight_worth_of(refund2), &xcm_context()),
Some(refund_asset_2)
);
assert_eq!(trader.total_fee.peek(), fee1 + fee2 - refund1 - refund2);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET_2)));
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee1);
assert_eq!(
Fungibles::total_issuance(CLIENT_ASSET_2),
client_total_2 + fee2 - refund1 - refund2
);
}
#[test]
fn not_enough_to_refund() {
let client_asset_total = 15;
let fee = 5;
let refund = 6;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn not_exchangeable_to_refund() {
let client_asset_total = 15;
let fee = 5;
let refund = 1;
setup_pool(CLIENT_ASSET, 1000, TARGET_ASSET, 1000);
let holding_asset = create_holding_asset(CLIENT_ASSET, client_asset_total);
let holding_change = create_holding_asset(CLIENT_ASSET, client_asset_total - fee);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader.buy_weight(weight_worth_of(fee), holding_asset, &xcm_context()).unwrap(),
holding_change
);
assert_eq!(trader.total_fee.peek(), fee);
assert_eq!(trader.last_fee_asset, Some(create_asset_id(CLIENT_ASSET)));
assert_eq!(trader.refund_weight(weight_worth_of(refund), &xcm_context()), None);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total + fee);
}
#[test]
fn nothing_to_refund() {
let fee = 5;
let mut trader = Trader::new();
assert_eq!(trader.refund_weight(weight_worth_of(fee), &xcm_context()), None);
}
#[test]
fn holding_asset_not_exchangeable_for_target() {
let holding_asset = create_holding_asset(CLIENT_ASSET, 10);
let target_total = Fungibles::total_issuance(TARGET_ASSET);
let client_total = Fungibles::total_issuance(CLIENT_ASSET);
let mut trader = Trader::new();
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
.unwrap_err(),
XcmError::FeesNotMet
);
assert_eq!(Fungibles::total_issuance(TARGET_ASSET), target_total);
assert_eq!(Fungibles::total_issuance(CLIENT_ASSET), client_total);
}
#[test]
fn empty_holding_asset() {
let mut trader = Trader::new();
assert_eq!(
trader
.buy_weight(Weight::from_all(10), AssetsInHolding::new(), &xcm_context())
.unwrap_err(),
XcmError::AssetNotFound
);
}
#[test]
fn fails_to_match_holding_asset() {
let mut trader = Trader::new();
let holding_asset = Asset { id: AssetId(Location::new(1, [Teyrchain(1)])), fun: Fungible(10) };
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset.into(), &xcm_context())
.unwrap_err(),
XcmError::AssetNotFound
);
}
#[test]
fn holding_asset_equal_to_target_asset() {
let mut trader = Trader::new();
let holding_asset = create_holding_asset(TargetAsset::get(), 10);
assert_eq!(
trader
.buy_weight(Weight::from_all(10), holding_asset, &xcm_context())
.unwrap_err(),
XcmError::FeesNotMet
);
}
pub mod mock {
use crate::*;
use core::cell::RefCell;
use pezframe_support::{
ensure,
traits::{
fungibles::{Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Unbalanced},
tokens::{
DepositConsequence, Fortitude, Fortitude::Polite, Precision::Exact, Preservation,
Preservation::Preserve, Provenance, WithdrawConsequence,
},
},
};
use pezsp_runtime::{traits::One, DispatchError};
use std::collections::HashMap;
use xcm::latest::Junction;
pub type AccountId = u64;
pub type AssetId = u32;
pub type Balance = u128;
pub type Credit = fungibles::Credit<AccountId, Fungibles>;
thread_local! {
pub static TOTAL_ISSUANCE: RefCell<HashMap<AssetId, Balance>> = RefCell::new(HashMap::new());
pub static ACCOUNT: RefCell<HashMap<(AssetId, AccountId), Balance>> = RefCell::new(HashMap::new());
pub static SWAP: RefCell<HashMap<(AssetId, AssetId), AccountId>> = RefCell::new(HashMap::new());
}
pub struct Swap {}
impl SwapCreditT<AccountId> for Swap {
type Balance = Balance;
type AssetKind = AssetId;
type Credit = Credit;
fn max_path_len() -> u32 {
2
}
fn swap_exact_tokens_for_tokens(
path: Vec<Self::AssetKind>,
credit_in: Self::Credit,
amount_out_min: Option<Self::Balance>,
) -> Result<Self::Credit, (Self::Credit, DispatchError)> {
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
ensure!(
credit_in.peek() >= amount_out_min.unwrap_or(Self::Balance::zero()),
(credit_in, DispatchError::Unavailable)
);
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
let pool_account = match swap_res {
Some(a) => a,
None => return Err((credit_in, DispatchError::Unavailable)),
};
let credit_out = match Fungibles::withdraw(
path[1],
&pool_account,
credit_in.peek(),
Exact,
Preserve,
Polite,
) {
Ok(c) => c,
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
};
Fungibles::resolve(&pool_account, credit_in)
.map_err(|c| (c, DispatchError::Unavailable))?;
Ok(credit_out)
}
fn swap_tokens_for_exact_tokens(
path: Vec<Self::AssetKind>,
credit_in: Self::Credit,
amount_out: Self::Balance,
) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)> {
ensure!(2 == path.len(), (credit_in, DispatchError::Unavailable));
ensure!(credit_in.peek() >= amount_out, (credit_in, DispatchError::Unavailable));
let swap_res = SWAP.with(|b| b.borrow().get(&(path[0], path[1])).map(|v| *v));
let pool_account = match swap_res {
Some(a) => a,
None => return Err((credit_in, DispatchError::Unavailable)),
};
let credit_out = match Fungibles::withdraw(
path[1],
&pool_account,
amount_out,
Exact,
Preserve,
Polite,
) {
Ok(c) => c,
Err(_) => return Err((credit_in, DispatchError::Unavailable)),
};
let (credit_in, change) = credit_in.split(amount_out);
Fungibles::resolve(&pool_account, credit_in)
.map_err(|c| (c, DispatchError::Unavailable))?;
Ok((credit_out, change))
}
}
pub fn pool_account(asset1: AssetId, asset2: AssetId) -> AccountId {
(1000 + asset1 * 10 + asset2 * 100).into()
}
pub fn setup_pool(asset1: AssetId, liquidity1: Balance, asset2: AssetId, liquidity2: Balance) {
let account = pool_account(asset1, asset2);
SWAP.with(|b| b.borrow_mut().insert((asset1, asset2), account));
let debt1 = Fungibles::deposit(asset1, &account, liquidity1, Exact);
let debt2 = Fungibles::deposit(asset2, &account, liquidity2, Exact);
drop(debt1);
drop(debt2);
}
pub struct WeightToFee;
impl WeightToFeeT for WeightToFee {
type Balance = Balance;
fn weight_to_fee(weight: &Weight) -> Self::Balance {
(weight.ref_time() + weight.proof_size()).into()
}
}
pub struct Fungibles {}
impl Inspect<AccountId> for Fungibles {
type AssetId = AssetId;
type Balance = Balance;
fn total_issuance(asset: Self::AssetId) -> Self::Balance {
TOTAL_ISSUANCE.with(|b| b.borrow().get(&asset).map_or(Self::Balance::zero(), |b| *b))
}
fn minimum_balance(_: Self::AssetId) -> Self::Balance {
Self::Balance::one()
}
fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn reducible_balance(
asset: Self::AssetId,
who: &AccountId,
_: Preservation,
_: Fortitude,
) -> Self::Balance {
ACCOUNT.with(|b| b.borrow().get(&(asset, *who)).map_or(Self::Balance::zero(), |b| *b))
}
fn can_deposit(
_: Self::AssetId,
_: &AccountId,
_: Self::Balance,
_: Provenance,
) -> DepositConsequence {
unimplemented!()
}
fn can_withdraw(
_: Self::AssetId,
_: &AccountId,
_: Self::Balance,
) -> WithdrawConsequence<Self::Balance> {
unimplemented!()
}
fn asset_exists(_: Self::AssetId) -> bool {
unimplemented!()
}
}
impl Unbalanced<AccountId> for Fungibles {
fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) {
TOTAL_ISSUANCE.with(|b| b.borrow_mut().insert(asset, amount));
}
fn handle_dust(_: Dust<AccountId, Self>) {
unimplemented!()
}
fn write_balance(
asset: Self::AssetId,
who: &AccountId,
amount: Self::Balance,
) -> Result<Option<Self::Balance>, DispatchError> {
let _ = ACCOUNT.with(|b| b.borrow_mut().insert((asset, *who), amount));
Ok(None)
}
}
impl Balanced<AccountId> for Fungibles {
type OnDropCredit = DecreaseIssuance<AccountId, Self>;
type OnDropDebt = IncreaseIssuance<AccountId, Self>;
}
pub struct FungiblesMatcher;
impl MatchesFungibles<AssetId, Balance> for FungiblesMatcher {
fn matches_fungibles(
a: &Asset,
) -> core::result::Result<(AssetId, Balance), xcm_executor::traits::Error> {
match a {
Asset { fun: Fungible(amount), id: AssetId(inner_location) } =>
match inner_location.unpack() {
(0, [Junction::GeneralIndex(id)]) =>
Ok(((*id).try_into().unwrap(), *amount)),
_ => Err(xcm_executor::traits::Error::AssetNotHandled),
},
_ => Err(xcm_executor::traits::Error::AssetNotHandled),
}
}
}
}