Compare commits

..

6 Commits

Author SHA1 Message Date
0e228d890d
rustfmt changes
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-18 14:41:58 +03:00
5e17c12677
use new logic from slow-clap for casper runtime
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-18 14:40:52 +03:00
c112ad22c2
apply eviction logic during the offence preparation
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-18 14:38:34 +03:00
0dd27d6429
eviction trait added
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-18 14:36:51 +03:00
8252107e96
make multiplication to round up during slash fraction calculation
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-10 11:14:06 +03:00
6419d4ba16
add hoodi network for the rpc-tester
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-06-10 11:11:46 +03:00
139 changed files with 8598 additions and 39508 deletions

1219
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ homepage.workspace = true
[workspace.package]
license = "GPL-3.0-only"
authors = ["571nky", "57r37ch", "f4750"]
version = "0.9.0"
version = "0.8.5"
edition = "2021"
homepage = "https://ghostchain.io"
repository = "https://git.ghostchain.io/ghostchain/ghost-node"
@ -67,21 +67,18 @@ serial_test = { version = "2.0.0" }
color-eyre = { version = "0.6.1" }
bs58 = { version = "0.5.0" }
prometheus-parse = { version = "0.2.2" }
k256 = { version = "0.13.0", default-features = false }
rustc-hex = { version = "2.1.0", default-features = false }
log = { version = "0.4", default-features = false }
num-traits = { version = "0.2.17", default-features = false }
libsecp256k1 = { version = "0.7", default-features = false }
bip39 = { package = "parity-bip39", version = "2.0.1" }
sha3 = { version = "0.10", default-features = false }
sha2 = { version = "0.10.7", default-features = false }
serde = { version = "1.0.197", default-features = false }
serde_derive = { version = "1.0.117", default-features = false }
serde_json = { version = "1.0.114", default-features = false }
blake2-rfc = { version = "0.2.18", default-features = false }
impl-serde = { version = "0.4.0", default-features = false }
hex = { version = "0.4.3", default-features = false }
strum = { version = "0.28.0", default-features = false }
metered = { package = "prioritized-metered-channel", version = "0.6.1", default-features = false }
scale-value = { version = "0.16.0" }
@ -89,13 +86,6 @@ codec = { package = "parity-scale-codec", version = "3.6.1", default-features =
scale-info = { version = "2.5.0", default-features = false }
subxt = { version = "0.37.0", default-features = false }
frost-secp256k1 = { version = "2.2.0", default-features = false, features = ["serialization"] }
frost-ed25519 = { version = "2.2.0", default-features = false, features = ["serialization"] }
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc"] }
frost-core = { version = "2.2.0", default-features = false }
rand_chacha = { version = "0.3.0", default-features = false }
hkdf = { version = "0.12.4", default-features = false }
# Frames
frame-support = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
@ -151,7 +141,6 @@ pallet-authority-discovery = { git = "https://github.com/paritytech/polkadot-sdk
pallet-authorship = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-babe = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-bags-list = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-bags-list-remote-tests = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-bounties = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-child-bounties = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
@ -166,6 +155,8 @@ pallet-grandpa = { git = "https://github.com/paritytech/polkadot-sdk.git", tag =
pallet-identity = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-indices = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-multisig = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-nomination-pools = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-nomination-pools-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-offences = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-preimage = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-proxy = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
@ -193,6 +184,8 @@ generate-bags = { git = "https://github.com/paritytech/polkadot-sdk.git", tag =
pallet-election-provider-support-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-offences-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-session-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-nomination-pools-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
pallet-bags-list-remote-tests = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0", default-features = false }
# Substrate Client dependencies
sc-cli = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.12.0" }
@ -234,22 +227,15 @@ ghost-metrics = { path = "metrics", default-features = false }
ghost-rpc = { path = "rpc", default-features = false }
service = { package = "ghost-service", path = "service", default-features = false }
ghost-traits = { path = "pallets/traits", default-features = false }
ghost-helpers = { path = "pallets/helpers", default-features = false }
ghost-networks = { path = "pallets/networks", default-features = false }
ghost-governance = { path = "pallets/governance", default-features = false }
ghost-weaver = { path = "pallets/weaver", default-features = false }
ghost-exodus = { path = "pallets/exodus", default-features = false }
ghost-claims = { path = "pallets/claims", default-features = false }
ghost-slow-clap = { path = "pallets/slow-clap", default-features = false }
ghost-sudo = { path = "pallets/sudo", default-features = false }
ghost-nomination-pools = { path = "pallets/pools", default-features = false }
ghost-nomination-pools-runtime-api = { path = "pallets/pools/runtime-api", default-features = false }
ghost-nomination-pools-benchmarking = { path = "pallets/pools/benchmarking", default-features = false }
ghost-nomination-pools-fuzzer = { path = "pallets/pools/benchmarking", default-features = false }
ghost-runtime-constants = { package = "ghost-runtime-constants", path = "runtime/ghost/constants", default-features = false }
casper-runtime = { path = "runtime/casper", default-features = false }
casper-runtime-constants = { package = "casper-runtime-constants", path = "runtime/casper/constants", default-features = false }
primitives = { package = "ghost-core-primitives", path = "core-primitives", default-features = false }
runtime-common = { package = "runtime-common", path = "runtime/common", default-features = false }
runtime-common = { package = "ghost-runtime-common", path = "runtime/common", default-features = false }
[dependencies]
color-eyre = { workspace = true }
@ -285,13 +271,9 @@ members = [
"runtime/casper",
"runtime/casper/constants",
"pallets/networks",
"pallets/governance",
"pallets/pools",
"pallets/claims",
"pallets/slow-clap",
"pallets/sudo",
"pallets/exodus",
"pallets/exodus/runtime-api",
"pallets/helpers",
"pallets/weaver",
"utils/bags-list",
"utils/chain-spec-builder",
"utils/generate-bags",
@ -344,12 +326,9 @@ crypto-mac = { opt-level = 3 }
curve25519-dalek = { opt-level = 3 }
ed25519-dalek = { opt-level = 3 }
flate2 = { opt-level = 3 }
frost-core = { opt-level = 3 }
frost-secp256k1 = { opt-level = 3 }
futures-channel = { opt-level = 3 }
hash-db = { opt-level = 3 }
hashbrown = { opt-level = 3 }
hkdf = { opt-level = 3 }
hmac = { opt-level = 3 }
httparse = { opt-level = 3 }
integer-sqrt = { opt-level = 3 }
@ -365,7 +344,6 @@ parking_lot = { opt-level = 3 }
parking_lot_core = { opt-level = 3 }
percent-encoding = { opt-level = 3 }
primitive-types = { opt-level = 3 }
rand_chacha = { opt-level = 3 }
ring = { opt-level = 3 }
rustls = { opt-level = 3 }
sha2 = { opt-level = 3 }

View File

@ -67,11 +67,11 @@ impl SubstrateCli for Cli {
#[cfg(feature = "casper-native")]
"casper" => Box::new(service::chain_spec::casper_config()?),
#[cfg(feature = "casper-native")]
"casper-dev" | "casper-development" => {
"casper-dev" | "dev" | "development" => {
Box::new(service::chain_spec::casper_development_config()?)
}
#[cfg(feature = "casper-native")]
"casper-local" => {
"casper-local" | "local" => {
Box::new(service::chain_spec::casper_local_testnet_config()?)
}
#[cfg(feature = "casper-native")]
@ -126,10 +126,7 @@ where
set_ss58_version(chain_spec);
runner.run_node_until_exit(move |mut config| async move {
config.offchain_worker.enabled = true;
config.offchain_worker.indexing_enabled = true;
runner.run_node_until_exit(move |config| async move {
let hwbench = (!cli.run.no_hardware_benchmarks)
.then_some(config.database.path().map(|database_path| {
let _ = std::fs::create_dir_all(&database_path);

View File

@ -1,7 +1,7 @@
[package]
name = "ghost-governance"
version = "0.3.0"
description = "Full-chain and cross-chain governance pallet with early adopter share claims"
name = "ghost-claims"
version = "0.2.4"
description = "Ghost balance and rank claims based on EVM actions"
license.workspace = true
authors.workspace = true
edition.workspace = true
@ -20,6 +20,8 @@ frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-ranked-collective = { workspace = true }
pallet-vesting = { workspace = true }
pallet-balances = { workspace = true }
sp-core = { features = ["serde"], workspace = true }
@ -27,15 +29,10 @@ sp-runtime = { features = ["serde"], workspace = true }
sp-io = { workspace = true }
sp-std = { workspace = true }
ghost-traits = { workspace = true }
ghost-helpers = { workspace = true }
ghost-networks = { workspace = true }
[dev-dependencies]
hex-literal = { workspace = true, default-features = true }
libsecp256k1 = { workspace = true, default-features = true }
hex = { workspace = true }
# hex-literal = { workspace = true, default-features = true }
# serde_json = { workspace = true, default-features = true }
serde_json = { workspace = true, default-features = true }
[features]
default = ["std"]
@ -43,7 +40,6 @@ std = [
"frame-benchmarking?/std",
"serde/std",
"codec/std",
"hex/std",
"scale-info/std",
"libsecp256k1/std",
"frame-support/std",
@ -52,11 +48,10 @@ std = [
"sp-runtime/std",
"sp-io/std",
"sp-std/std",
"pallet-ranked-collective/std",
"pallet-vesting/std",
"pallet-balances/std",
"rustc-hex/std",
"ghost-traits/std",
"ghost-helpers/std",
"ghost-networks/std",
]
runtime-benchmarks = [
"libsecp256k1/hmac",
@ -64,13 +59,16 @@ runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-ranked-collective/runtime-benchmarks",
"pallet-vesting/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"ghost-networks/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-ranked-collective/try-runtime",
"pallet-vesting/try-runtime",
"pallet-balances/try-runtime",
"sp-runtime/try-runtime",
]

View File

@ -0,0 +1,59 @@
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
#[instance_benchmarks]
mod benchmarks {
use super::*;
use frame_support::dispatch::RawOrigin;
use pallet_ranked_collective::Pallet as Club;
#[benchmark]
fn claim() {
let i = 1337u32;
let ethereum_secret_key =
libsecp256k1::SecretKey::parse(&keccak_256(&i.to_le_bytes())).unwrap();
let eth_address = crate::secp_utils::eth(&ethereum_secret_key);
let balance = CurrencyOf::<T, I>::minimum_balance();
let pseudo_account: T::AccountId = Pallet::<T, I>::into_account_id(eth_address).unwrap();
let _ = CurrencyOf::<T, I>::deposit_creating(&pseudo_account, balance);
Total::<T, I>::put(balance);
let pseudo_rank = 5u16;
let _ = Club::<T, I>::do_add_member_to_rank(pseudo_account.clone(), pseudo_rank, false);
let user_account: T::AccountId = account("user", i, 0);
let signature =
crate::secp_utils::sig::<T, I>(&ethereum_secret_key, &user_account.encode());
let prev_balance = CurrencyOf::<T, I>::free_balance(&user_account);
let prev_rank = Club::<T, I>::rank_of(&user_account);
#[extrinsic_call]
claim(
RawOrigin::Signed(user_account.clone()),
eth_address,
signature,
);
assert_eq!(
CurrencyOf::<T, I>::free_balance(&user_account),
prev_balance + balance
);
assert_eq!(
CurrencyOf::<T, I>::free_balance(&pseudo_account),
balance - balance
);
let rank = match prev_rank {
Some(current_rank) if pseudo_rank <= current_rank => Some(current_rank),
_ => Some(pseudo_rank),
};
assert_eq!(Club::<T, I>::rank_of(&user_account), rank);
assert_eq!(Club::<T, I>::rank_of(&pseudo_account), None);
}
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test,);
}

336
pallets/claims/src/lib.rs Normal file
View File

@ -0,0 +1,336 @@
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
ensure,
pallet_prelude::*,
traits::{
Currency, ExistenceRequirement, Get, RankedMembers, RankedMembersSwapHandler,
VestingSchedule,
},
DefaultNoBound,
};
use frame_system::pallet_prelude::*;
pub use pallet::*;
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
use sp_runtime::traits::{BlockNumberProvider, CheckedDiv, CheckedSub};
use sp_std::prelude::*;
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, string::String};
mod weights;
pub use crate::weights::WeightInfo;
mod benchmarking;
mod mock;
mod secp_utils;
mod tests;
/// An ethereum address (i.e. 20 bytes, used to represent an Ethereum account).
///
/// This gets serialized to the 0x-prefixed hex representation.
#[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, Default, RuntimeDebug, TypeInfo)]
pub struct EthereumAddress(pub [u8; 20]);
impl Serialize for EthereumAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex: String = rustc_hex::ToHex::to_hex(&self.0[..]);
serializer.serialize_str(&format!("0x{}", hex))
}
}
impl<'de> Deserialize<'de> for EthereumAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let base_string = String::deserialize(deserializer)?;
let offset = if base_string.starts_with("0x") { 2 } else { 0 };
let s = &base_string[offset..];
if s.len() != 40 {
Err(serde::de::Error::custom(
"Bad length of Ethereum address (should be 42 including `0x`)",
))?;
}
let raw: Vec<u8> = rustc_hex::FromHex::from_hex(s)
.map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?;
let mut r = Self::default();
r.0.copy_from_slice(&raw);
Ok(r)
}
}
#[derive(Encode, Decode, Clone, TypeInfo)]
pub struct EcdsaSignature(pub [u8; 65]);
impl PartialEq for EcdsaSignature {
fn eq(&self, other: &Self) -> bool {
&self.0[..] == &other.0[..]
}
}
impl sp_std::fmt::Debug for EcdsaSignature {
fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
write!(f, "EcdsaSignature({:?})", &self.0[..])
}
}
type CurrencyOf<T, I> = <<T as Config<I>>::VestingSchedule as VestingSchedule<
<T as frame_system::Config>::AccountId,
>>::Currency;
type BalanceOf<T, I> =
<CurrencyOf<T, I> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type RankOf<T, I> = <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::Rank;
type AccountIdOf<T, I> = <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::AccountId;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T, I = ()>(_);
#[pallet::config]
pub trait Config<I: 'static = ()>:
frame_system::Config + pallet_ranked_collective::Config<I>
{
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type VestingSchedule: VestingSchedule<Self::AccountId, Moment = BlockNumberFor<Self>>;
type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
type MemberSwappedHandler: RankedMembersSwapHandler<AccountIdOf<Self, I>, RankOf<Self, I>>;
#[pallet::constant]
type Prefix: Get<&'static [u8]>;
#[pallet::constant]
type MaximumWithdrawAmount: Get<BalanceOf<Self, I>>;
#[pallet::constant]
type VestingBlocks: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
Claimed {
receiver: T::AccountId,
donor: T::AccountId,
amount: BalanceOf<T, I>,
rank: Option<RankOf<T, I>>,
},
}
#[pallet::error]
pub enum Error<T, I = ()> {
InvalidEthereumSignature,
InvalidEthereumAddress,
NoBalanceToClaim,
AddressDecodingFailed,
ArithmeticError,
PotUnderflow,
}
#[pallet::storage]
pub type Total<T: Config<I>, I: 'static = ()> = StorageValue<_, BalanceOf<T, I>, ValueQuery>;
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub total: BalanceOf<T, I>,
pub members_and_ranks: Vec<(T::AccountId, u16)>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
let cult_accounts: Vec<_> = self
.members_and_ranks
.iter()
.map(|(account_id, rank)| {
assert!(
pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(
account_id.clone(),
*rank,
false
)
.is_ok(),
"error during adding and promotion"
);
account_id
})
.collect();
assert!(
self.members_and_ranks.len() == cult_accounts.len(),
"duplicates in `members_and_ranks`"
);
Total::<T, I>::put(self.total);
}
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config<I>>::WeightInfo::claim())]
pub fn claim(
origin: OriginFor<T>,
ethereum_address: EthereumAddress,
ethereum_signature: EcdsaSignature,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let data = who.using_encoded(to_ascii_hex);
let recovered_address = Self::recover_ethereum_address(&ethereum_signature, &data)
.ok_or(Error::<T, I>::InvalidEthereumSignature)?;
ensure!(
recovered_address == ethereum_address,
Error::<T, I>::InvalidEthereumAddress
);
Self::do_claim(who, ethereum_address)
}
}
}
fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
let mut r = Vec::with_capacity(data.len() * 2);
let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
for &b in data.iter() {
push_nibble(b / 16);
push_nibble(b % 16);
}
r
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn ethereum_signable_message(what: &[u8]) -> Vec<u8> {
let prefix = T::Prefix::get();
let mut l = prefix.len() + what.len();
let mut rev = Vec::new();
while l > 0 {
rev.push(b'0' + (l % 10) as u8);
l /= 10;
}
let mut v = b"\x19Ethereum Signed Message:\n".to_vec();
v.extend(rev.into_iter().rev());
v.extend_from_slice(prefix);
v.extend_from_slice(what);
v
}
fn recover_ethereum_address(s: &EcdsaSignature, what: &[u8]) -> Option<EthereumAddress> {
let msg = keccak_256(&Self::ethereum_signable_message(what));
let mut res = EthereumAddress::default();
res.0
.copy_from_slice(&keccak_256(&secp256k1_ecdsa_recover(&s.0, &msg).ok()?[..])[12..]);
Some(res)
}
fn into_account_id(address: EthereumAddress) -> Result<T::AccountId, codec::Error> {
let mut data = [0u8; 32];
data[0..4].copy_from_slice(b"evm:");
data[4..24].copy_from_slice(&address.0[..]);
let hash = sp_core::keccak_256(&data);
T::AccountId::decode(&mut &hash[..])
}
fn do_claim(receiver: T::AccountId, ethereum_address: EthereumAddress) -> DispatchResult {
let donor = Self::into_account_id(ethereum_address)
.ok()
.ok_or(Error::<T, I>::AddressDecodingFailed)?;
let balance_due = CurrencyOf::<T, I>::free_balance(&donor);
ensure!(
balance_due >= CurrencyOf::<T, I>::minimum_balance(),
Error::<T, I>::NoBalanceToClaim
);
let new_total = Total::<T, I>::get()
.checked_sub(&balance_due)
.ok_or(Error::<T, I>::PotUnderflow)?;
CurrencyOf::<T, I>::transfer(
&donor,
&receiver,
balance_due,
ExistenceRequirement::AllowDeath,
)?;
let max_amount = T::MaximumWithdrawAmount::get();
if balance_due > max_amount {
let vesting_balance = balance_due
.checked_sub(&max_amount)
.ok_or(Error::<T, I>::ArithmeticError)?;
let vesting_blocks = T::VestingBlocks::get();
let per_block_balance = vesting_balance
.checked_div(&vesting_blocks.into())
.ok_or(Error::<T, I>::ArithmeticError)?;
T::VestingSchedule::add_vesting_schedule(
&receiver,
vesting_balance,
per_block_balance,
T::BlockNumberProvider::current_block_number(),
)?;
}
let rank = if let Some(rank) =
<pallet_ranked_collective::Pallet<T, I> as RankedMembers>::rank_of(&donor)
{
pallet_ranked_collective::Pallet::<T, I>::do_remove_member_from_rank(&donor, rank)?;
let new_rank =
match <pallet_ranked_collective::Pallet<T, I> as RankedMembers>::rank_of(&receiver)
{
Some(current_rank) if current_rank >= rank => current_rank,
Some(current_rank) if current_rank < rank => {
for _ in 0..rank - current_rank {
pallet_ranked_collective::Pallet::<T, I>::do_promote_member(
receiver.clone(),
None,
false,
)?;
}
rank
}
_ => {
pallet_ranked_collective::Pallet::<T, I>::do_add_member_to_rank(
receiver.clone(),
rank,
false,
)?;
rank
}
};
<T as pallet::Config<I>>::MemberSwappedHandler::swapped(&donor, &receiver, new_rank);
Some(new_rank)
} else {
None
};
Total::<T, I>::put(new_total);
Self::deposit_event(Event::<T, I>::Claimed {
receiver,
donor,
amount: balance_due,
rank,
});
Ok(())
}
}

281
pallets/claims/src/mock.rs Normal file
View File

@ -0,0 +1,281 @@
#![cfg(test)]
use super::*;
pub use crate as ghost_claims;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU16, ConstU64, PollStatus, Polling, WithdrawReasons},
};
use frame_system::EnsureRootWithSuccess;
pub use pallet_ranked_collective::{Rank, TallyOf};
use sp_runtime::{traits::Convert, BuildStorage};
pub mod eth_keys {
use crate::{mock::Test, EcdsaSignature, EthereumAddress};
use codec::Encode;
use hex_literal::hex;
pub fn total_claims() -> u64 {
10 + 100 + 1000
}
pub fn alice_account_id() -> <Test as frame_system::Config>::AccountId {
69
}
pub fn bob_account_id() -> <Test as frame_system::Config>::AccountId {
1337
}
pub fn first_eth_public_known() -> EthereumAddress {
EthereumAddress(hex!("1A69d2D5568D1878023EeB121a73d33B9116A760"))
}
pub fn second_eth_public_known() -> EthereumAddress {
EthereumAddress(hex!("2f86cfBED3fbc1eCf2989B9aE5fc019a837A9C12"))
}
pub fn third_eth_public_known() -> EthereumAddress {
EthereumAddress(hex!("e83f67361Ac74D42A48E2DAfb6706eb047D8218D"))
}
pub fn fourth_eth_public_known() -> EthereumAddress {
EthereumAddress(hex!("827ee4ad9b259b6fa1390ed60921508c78befd63"))
}
fn first_eth_private_key() -> libsecp256k1::SecretKey {
libsecp256k1::SecretKey::parse(&hex!(
"01c928771aea942a1e7ac06adf2b73dfbc9a25d9eaa516e3673116af7f345198"
))
.unwrap()
}
fn second_eth_private_key() -> libsecp256k1::SecretKey {
libsecp256k1::SecretKey::parse(&hex!(
"b19a435901872f817185f7234a1484eae837613f9d10cf21927a23c2d8cb9139"
))
.unwrap()
}
fn third_eth_private_key() -> libsecp256k1::SecretKey {
libsecp256k1::SecretKey::parse(&hex!(
"d3baf57b74d65719b2dc33f5a464176022d0cc5edbca002234229f3e733875fc"
))
.unwrap()
}
fn fourth_eth_private_key() -> libsecp256k1::SecretKey {
libsecp256k1::SecretKey::parse(&hex!(
"c4683d566436af6b58b4a59c8f501319226e85b21869bf93d5eeb4596d4791d4"
))
.unwrap()
}
fn wrong_eth_private_key() -> libsecp256k1::SecretKey {
libsecp256k1::SecretKey::parse(&hex!(
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
))
.unwrap()
}
pub fn first_eth_public_key() -> EthereumAddress {
crate::secp_utils::eth(&first_eth_private_key())
}
pub fn second_eth_public_key() -> EthereumAddress {
crate::secp_utils::eth(&second_eth_private_key())
}
pub fn third_eth_public_key() -> EthereumAddress {
crate::secp_utils::eth(&third_eth_private_key())
}
pub fn fourth_eth_public_key() -> EthereumAddress {
crate::secp_utils::eth(&fourth_eth_private_key())
}
pub fn first_account_id() -> <Test as frame_system::Config>::AccountId {
crate::secp_utils::into_account_id::<Test, ()>(first_eth_public_key())
}
pub fn second_account_id() -> <Test as frame_system::Config>::AccountId {
crate::secp_utils::into_account_id::<Test, ()>(second_eth_public_key())
}
pub fn third_account_id() -> <Test as frame_system::Config>::AccountId {
crate::secp_utils::into_account_id::<Test, ()>(third_eth_public_key())
}
pub fn fourth_account_id() -> <Test as frame_system::Config>::AccountId {
crate::secp_utils::into_account_id::<Test, ()>(fourth_eth_public_key())
}
pub fn first_signature() -> EcdsaSignature {
crate::secp_utils::sig::<Test, ()>(&first_eth_private_key(), &alice_account_id().encode())
}
pub fn second_signature() -> EcdsaSignature {
crate::secp_utils::sig::<Test, ()>(&second_eth_private_key(), &alice_account_id().encode())
}
pub fn third_signature() -> EcdsaSignature {
crate::secp_utils::sig::<Test, ()>(&third_eth_private_key(), &alice_account_id().encode())
}
pub fn fourth_signature() -> EcdsaSignature {
crate::secp_utils::sig::<Test, ()>(&fourth_eth_private_key(), &bob_account_id().encode())
}
pub fn wrong_signature() -> EcdsaSignature {
crate::secp_utils::sig::<Test, ()>(&wrong_eth_private_key(), &alice_account_id().encode())
}
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type AccountData = pallet_balances::AccountData<u64>;
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl pallet_balances::Config for Test {
type AccountStore = System;
}
parameter_types! {
pub const MinVestedTransfer: u64 = 1;
pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons =
WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE);
}
impl pallet_vesting::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type BlockNumberToBalance = sp_runtime::traits::ConvertInto;
type MinVestedTransfer = MinVestedTransfer;
type WeightInfo = ();
type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons;
type BlockNumberProvider = System;
const MAX_VESTING_SCHEDULES: u32 = 28;
}
parameter_types! {
pub MinRankOfClassDelta: Rank = 1;
}
pub struct MinRankOfClass<Delta>(sp_std::marker::PhantomData<Delta>);
impl<Delta: Get<Rank>> Convert<Class, Rank> for MinRankOfClass<Delta> {
fn convert(a: Class) -> Rank {
a.saturating_sub(Delta::get())
}
}
pub struct TestPolls;
impl Polling<TallyOf<Test>> for TestPolls {
type Index = u8;
type Votes = u32;
type Moment = u64;
type Class = Class;
fn classes() -> Vec<Self::Class> {
unimplemented!()
}
fn as_ongoing(_index: u8) -> Option<(TallyOf<Test>, Self::Class)> {
unimplemented!()
}
fn access_poll<R>(
_index: Self::Index,
_f: impl FnOnce(PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>) -> R,
) -> R {
unimplemented!()
}
fn try_access_poll<R>(
_index: Self::Index,
_f: impl FnOnce(
PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>,
) -> Result<R, DispatchError>,
) -> Result<R, DispatchError> {
unimplemented!()
}
#[cfg(feature = "runtime-benchmarks")]
fn create_ongoing(_class: Self::Class) -> Result<Self::Index, ()> {
unimplemented!()
}
#[cfg(feature = "runtime-benchmarks")]
fn end_ongoing(_index: Self::Index, _approved: bool) -> Result<(), ()> {
unimplemented!()
}
}
impl pallet_ranked_collective::Config for Test {
type WeightInfo = ();
type RuntimeEvent = RuntimeEvent;
type AddOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
type RemoveOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
type PromoteOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
type DemoteOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
type ExchangeOrigin = EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>;
type Polls = TestPolls;
type MemberSwappedHandler = ();
type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>;
type VoteWeight = pallet_ranked_collective::Geometric;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkSetup = ();
}
parameter_types! {
pub Prefix: &'static [u8] = b"AccountId:";
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type VestingSchedule = Vesting;
type BlockNumberProvider = System;
type MemberSwappedHandler = ();
type Prefix = Prefix;
type MaximumWithdrawAmount = ConstU64<200>;
type VestingBlocks = ConstU32<80>;
type WeightInfo = ();
}
type Block = frame_system::mocking::MockBlock<Test>;
type Class = Rank;
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Balances: pallet_balances,
Vesting: pallet_vesting,
Club: pallet_ranked_collective,
Claims: ghost_claims,
}
);
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![
(crate::mock::eth_keys::first_account_id(), 10),
(crate::mock::eth_keys::second_account_id(), 100),
(crate::mock::eth_keys::third_account_id(), 1000),
],
}
.assimilate_storage(&mut t)
.unwrap();
ghost_claims::GenesisConfig::<Test> {
total: crate::mock::eth_keys::total_claims(),
members_and_ranks: vec![
(crate::mock::eth_keys::second_account_id(), 1),
(crate::mock::eth_keys::third_account_id(), 3),
],
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| {
System::set_block_number(1);
});
ext
}

View File

@ -0,0 +1,35 @@
#![cfg(any(test, feature = "runtime-benchmarks"))]
use crate::{keccak_256, Config, EcdsaSignature, EthereumAddress};
pub fn public(secret: &libsecp256k1::SecretKey) -> libsecp256k1::PublicKey {
libsecp256k1::PublicKey::from_secret_key(secret)
}
pub fn eth(secret: &libsecp256k1::SecretKey) -> EthereumAddress {
let mut res = EthereumAddress::default();
res.0
.copy_from_slice(&keccak_256(&public(secret).serialize()[1..65])[12..]);
res
}
#[cfg(test)]
pub fn into_account_id<T: Config<I>, I: 'static>(address: EthereumAddress) -> T::AccountId {
super::Pallet::<T, I>::into_account_id(address).unwrap()
}
pub fn sig<T: Config<I>, I: 'static>(
secret: &libsecp256k1::SecretKey,
what: &[u8],
) -> EcdsaSignature {
let msg = keccak_256(&super::Pallet::<T, I>::ethereum_signable_message(
&crate::to_ascii_hex(what)[..],
));
let (sig, recovery_id) = libsecp256k1::sign(&libsecp256k1::Message::parse(&msg), secret);
let mut r = [0u8; 65];
r[0..64].copy_from_slice(&sig.serialize()[..]);
r[64] = recovery_id.serialize();
EcdsaSignature(r)
}

285
pallets/claims/src/tests.rs Normal file
View File

@ -0,0 +1,285 @@
#![cfg(test)]
use super::*;
use frame_support::{assert_err, assert_ok};
use hex_literal::hex;
use mock::{
eth_keys::{
alice_account_id, bob_account_id, first_account_id, first_eth_public_key,
first_eth_public_known, first_signature, fourth_account_id, fourth_eth_public_key,
fourth_eth_public_known, fourth_signature, second_account_id, second_eth_public_key,
second_eth_public_known, second_signature, third_account_id, third_eth_public_key,
third_eth_public_known, third_signature, total_claims, wrong_signature,
},
ghost_claims, new_test_ext, Balances, Claims, Club, RuntimeEvent, RuntimeOrigin, System, Test,
Vesting,
};
#[test]
fn serde_works() {
let x = EthereumAddress(hex!["0123456789abcdef0123456789abcdef01234567"]);
let y = serde_json::to_string(&x).unwrap();
assert_eq!(y, "\"0x0123456789abcdef0123456789abcdef01234567\"");
let z: EthereumAddress = serde_json::from_str(&y).unwrap();
assert_eq!(x, z);
}
#[test]
fn known_eth_public_accounts_are_correct() {
assert_eq!(first_eth_public_key(), first_eth_public_known());
assert_eq!(second_eth_public_key(), second_eth_public_known());
assert_eq!(third_eth_public_key(), third_eth_public_known());
assert_eq!(fourth_eth_public_key(), fourth_eth_public_known());
}
#[test]
fn basic_setup_works() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
assert_eq!(Club::rank_of(&alice_account_id()), None);
assert_eq!(Club::rank_of(&first_account_id()), None);
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
});
}
#[test]
fn small_claiming_works() {
new_test_ext().execute_with(|| {
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
first_eth_public_key(),
first_signature()
));
assert_eq!(Balances::usable_balance(&alice_account_id()), 10);
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
assert_eq!(Club::rank_of(&alice_account_id()), None);
assert_eq!(Club::rank_of(&first_account_id()), None);
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims() - 10);
})
}
#[test]
fn medium_claiming_works() {
new_test_ext().execute_with(|| {
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
second_eth_public_key(),
second_signature(),
));
assert_eq!(Balances::usable_balance(&alice_account_id()), 100);
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
assert_eq!(Club::rank_of(&alice_account_id()), Some(1));
assert_eq!(Club::rank_of(&second_account_id()), None);
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims() - 100);
})
}
#[test]
fn big_claiming_works() {
new_test_ext().execute_with(|| {
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
third_eth_public_key(),
third_signature(),
));
assert_eq!(Balances::usable_balance(&alice_account_id()), 200);
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
assert_eq!(Club::rank_of(&third_account_id()), None);
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
assert_eq!(
ghost_claims::Total::<Test, ()>::get(),
total_claims() - 1000
);
assert_ok!(Balances::transfer_allow_death(
RuntimeOrigin::signed(alice_account_id()),
bob_account_id(),
200
));
})
}
#[test]
fn multiple_accounts_claiming_works() {
new_test_ext().execute_with(|| {
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
first_eth_public_key(),
first_signature(),
));
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
second_eth_public_key(),
second_signature(),
));
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
third_eth_public_key(),
third_signature(),
));
assert_eq!(Balances::usable_balance(&alice_account_id()), 310);
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
assert_eq!(Club::rank_of(&third_account_id()), None);
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
assert_eq!(ghost_claims::Total::<Test, ()>::get(), 0);
})
}
#[test]
fn multiple_accounts_reverese_claiming_works() {
new_test_ext().execute_with(|| {
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
third_eth_public_key(),
third_signature(),
));
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
second_eth_public_key(),
second_signature(),
));
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
first_eth_public_key(),
first_signature(),
));
assert_eq!(Balances::usable_balance(&alice_account_id()), 310);
assert_eq!(Balances::usable_balance(&first_account_id()), 0);
assert_eq!(Balances::usable_balance(&second_account_id()), 0);
assert_eq!(Balances::usable_balance(&third_account_id()), 0);
assert_eq!(Club::rank_of(&alice_account_id()), Some(3));
assert_eq!(Club::rank_of(&third_account_id()), None);
assert_eq!(Vesting::vesting_balance(&alice_account_id()), Some(800));
assert_eq!(ghost_claims::Total::<Test, ()>::get(), 0);
})
}
#[test]
fn cannot_claim_with_bad_signature() {
new_test_ext().execute_with(|| {
assert_err!(
Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
first_eth_public_key(),
wrong_signature()
),
crate::Error::<Test>::InvalidEthereumAddress
);
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
assert_eq!(Club::rank_of(&alice_account_id()), None);
assert_eq!(Club::rank_of(&first_account_id()), None);
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
})
}
#[test]
fn cannot_claim_with_wrong_address() {
new_test_ext().execute_with(|| {
assert_err!(
Claims::claim(
RuntimeOrigin::signed(bob_account_id()),
first_eth_public_key(),
first_signature()
),
crate::Error::<Test>::InvalidEthereumAddress
);
assert_eq!(Balances::usable_balance(&bob_account_id()), 0);
assert_eq!(Balances::usable_balance(&alice_account_id()), 0);
assert_eq!(Balances::usable_balance(&first_account_id()), 10);
assert_eq!(Balances::usable_balance(&second_account_id()), 100);
assert_eq!(Balances::usable_balance(&third_account_id()), 1000);
assert_eq!(Club::rank_of(&alice_account_id()), None);
assert_eq!(Club::rank_of(&first_account_id()), None);
assert_eq!(Club::rank_of(&second_account_id()), Some(1));
assert_eq!(Club::rank_of(&third_account_id()), Some(3));
assert_eq!(Vesting::vesting_balance(&alice_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
})
}
#[test]
fn cannot_claim_nothing() {
new_test_ext().execute_with(|| {
assert_err!(
Claims::claim(
RuntimeOrigin::signed(bob_account_id()),
fourth_eth_public_key(),
fourth_signature()
),
crate::Error::<Test>::NoBalanceToClaim
);
assert_eq!(Balances::usable_balance(&bob_account_id()), 0);
assert_eq!(Balances::usable_balance(&fourth_account_id()), 0);
assert_eq!(Vesting::vesting_balance(&bob_account_id()), None);
assert_eq!(ghost_claims::Total::<Test, ()>::get(), total_claims());
})
}
#[test]
fn event_emitted_during_claim() {
new_test_ext().execute_with(|| {
let account = third_account_id();
let amount = Balances::usable_balance(&account);
let rank = Club::rank_of(&account);
System::reset_events();
assert_eq!(System::event_count(), 0);
assert_ok!(Claims::claim(
RuntimeOrigin::signed(alice_account_id()),
third_eth_public_key(),
third_signature(),
));
System::assert_has_event(RuntimeEvent::Claims(crate::Event::Claimed {
receiver: alice_account_id(),
donor: account,
amount,
rank,
}));
})
}

View File

@ -1,108 +0,0 @@
[package]
name = "ghost-exodus"
version = "0.0.0"
description = "Threshold signature generation with DKG included"
license.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
log = { workspace = true }
hex = { workspace = true }
codec = { workspace = true, features = ["max-encoded-len"] }
scale-info = { workspace = true, features = ["derive"] }
strum = { workspace = true, features = ["derive"] }
num-traits = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-arithmetic = { workspace = true }
sp-application-crypto = { workspace = true }
sp-runtime = { workspace = true }
sp-staking = { workspace = true }
sp-core = { workspace = true }
sp-std = { workspace = true }
sp-io = { workspace = true }
frost-secp256k1 = { workspace = true }
frost-ed25519 = { workspace = true }
frost-core = { workspace = true }
rand_chacha = { workspace = true }
hkdf = { workspace = true }
chacha20poly1305 = { workspace = true }
sha2 = { workspace = true }
k256 = { workspace = true }
parking_lot = { workspace = true }
ghost-traits = { workspace = true }
ghost-helpers = { workspace = true }
pallet-balances = { workspace = true }
pallet-session = { workspace = true }
pallet-staking = { workspace = true }
ghost-networks = { workspace = true }
pallet-staking-reward-curve = { workspace = true }
[dev-dependencies]
sp-keystore = { workspace = true }
frame-executive = { workspace = true }
rand = { workspace = true }
[features]
default = ["std"]
std = [
"frame-benchmarking?/std",
"scale-info/std",
"strum/std",
"log/std",
"hex/std",
"k256/std",
"num-traits/std",
"frame-support/std",
"frame-system/std",
"sha2/std",
"hkdf/std",
"chacha20poly1305/std",
"rand_chacha/std",
"sp-arithmetic/std",
"sp-application-crypto/std",
"sp-runtime/std",
"sp-staking/std",
"sp-core/std",
"sp-std/std",
"sp-io/std",
"frost-secp256k1/std",
"frost-ed25519/std",
"frost-core/std",
"ghost-traits/std",
"ghost-helpers/std",
"pallet-balances/std",
"pallet-session/std",
"pallet-staking/std",
"ghost-networks/std",
"frame-executive/std",
"sp-keystore/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-staking/runtime-benchmarks",
"ghost-networks/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
"pallet-balances/try-runtime",
"pallet-session/try-runtime",
"frame-executive/try-runtime",
"ghost-networks/try-runtime",
]

View File

@ -1,20 +0,0 @@
[package]
name = "ghost-exodus-runtime-api"
version = "0.0.0"
description = "RPC runtime API for ghost EXODUS pallet"
license.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, features = ["derive"] }
sp-api = { workspace = true }
[features]
default = ["std"]
std = ["codec/std", "sp-api/std"]

View File

@ -1,7 +0,0 @@
#![cfg_attr(not(feature = "std"), no_std)]
sp_api::decl_runtime_apis! {
pub trait ExodusApi<NetworkId> {
fn verifying_key(network_id: NetworkId) -> Vec<u8>;
}
}

View File

@ -1,768 +0,0 @@
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use ghost_helpers::networks::NetworkDataBuilder;
fn prepare_pallet<T: Config>(authorities_len: usize) -> (
PendingDkgAuthorities<ParticipantsBitmap<T>, BlockNumberFor<T>>,
NetworkCurve,
T::AuthorityId,
AuthIndex
) {
let network_curve = NetworkCurve::Secp256k1;
let authority = T::AuthorityId::generate_pair(None);
let authorities = vec![authority.clone(); authorities_len];
let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone())
.expect("more than the maximum number of keys provided");
QualificationAuthorities::<T>::insert(network_curve, bounded_authorities);
let network_id = NetworkIdOf::<T>::default();
let network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Evm)
.with_network_curve(NetworkCurve::Secp256k1)
.build();
T::NetworkDataHandler::register(network_id, network_data)
.expect("network should be valid for insertion");
let mut dkg_state = PendingDkgAuthorities::default();
dkg_state.new_dkg(authorities_len);
(dkg_state, network_curve, authority, 0)
}
#[benchmarks(where T: Config)]
mod benchmarks {
use super::*;
#[benchmark]
fn register_round0_package() -> Result<(), BenchmarkError> {
let dummy_hash = ExodusHash::repeat_byte(69);
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(1);
dkg_state.next_phase();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let round0_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(0)
.with_dkg_round0(dummy_hash);
let dkg_package = DkgPackage::Round0(round0_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let expected_hash = Round0Packages::<T>::get(&network_curve, authority_index);
assert_eq!(expected_hash, dummy_hash);
Ok(())
}
#[benchmark]
fn register_round1_package(
c: Linear<4, { T::MaxAuthorities::get() }>
) -> Result<(), BenchmarkError> {
let max_signers = c as AuthIndex;
let min_signers = get_byzantium_threshold(max_signers);
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.insert_index(authority_index);
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let mut rng = Pallet::<T>::get_offchain_rng(authority_index);
let (_, public_package) = network_curve.dkg_part1(
authority_index,
max_signers,
min_signers,
&mut rng,
).expect("DKG part1 should work");
let package_hash = ExodusHash::from(blake2_256(&public_package));
Round0Packages::<T>::insert(network_curve, authority_index, package_hash);
let round1_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(0)
.with_dkg_round1(public_package)
.expect("Round1 package should be valid");
let dkg_package = DkgPackage::Round1(round1_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let participants = Round1Packages::<T>::get(&network_curve);
assert!(participants.contains(authority_index));
Ok(())
}
#[benchmark]
fn register_encrypted_round2_packages(
c: Linear<1, { T::MaxAuthorities::get() / 8 }>
) -> Result<(), BenchmarkError> {
let max_signers = (c * 8) as AuthIndex;
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
(0..max_signers).for_each(|i| {
dkg_state.insert_index(i);
});
let padded_participants = dkg_state
.highest_bit::<usize>()
.map(|bit_pos| bit_pos.saturating_add(1))
.unwrap_or_default();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let bitmask_len = padded_participants.div_ceil(8);
let blob_len = round2_encrypted_package_size(
padded_participants,
network_curve.scalar_bytes_len(),
network_curve.header_bytes_len(),
);
let mut bitmask = vec![0u8; bitmask_len];
let blob = vec![0u8; blob_len];
(0..max_signers).for_each(|i| {
if i != authority_index {
let byte_index = (i / 8) as usize;
let bit_index = (i % 8) as u8;
bitmask[byte_index] |= 1 << bit_index;
}
});
let encryption_bundle = BundledPackages { bitmask, blob };
let round2_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_dkg_round2(encryption_bundle)
.expect("Round2 package should be valid");
let dkg_package = DkgPackage::Round2(round2_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let participants = Round2Packages::<T>::get(&network_curve);
assert!(participants.contains(authority_index));
Ok(())
}
#[benchmark]
fn register_round3_complaints(
c: Linear<1, { T::MaxAuthorities::get() / 8 }>
) -> Result<(), BenchmarkError> {
let max_signers = (c * 8) as AuthIndex;
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
(0..max_signers).for_each(|i| {
dkg_state.insert_index(i);
});
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let bitvec_len = (max_signers + 7) / 8;
let mut accused_indexes = vec![0u8; bitvec_len as usize];
(0..max_signers).for_each(|i| {
if i != authority_index {
let byte_index = (i / 8) as usize;
let bit_index = (i % 8) as u8;
accused_indexes[byte_index] |= 1 << bit_index;
}
});
let round3_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_dkg_round3(accused_indexes)
.expect("Round3 package should be valid");
let dkg_package = DkgPackage::Round3(round3_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let complaints = Complaints::<T>::get(&network_curve);
let my_complaints = complaints.get(&authority_index)
.expect("Comlpaints should be registered");
assert_eq!(my_complaints.count_ones::<AuthIndex>(), max_signers - 1);
Ok(())
}
#[benchmark]
fn register_round4_justifications(
c: Linear<1, { T::MaxAuthorities::get() / 8 }>
) -> Result<(), BenchmarkError> {
let max_signers = (c * 8) as AuthIndex;
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
(0..max_signers).for_each(|i| {
dkg_state.insert_index(i);
});
let padded_participants = dkg_state
.highest_bit::<usize>()
.map(|bit_pos| bit_pos.saturating_add(1))
.unwrap_or_default();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let bitmask_len = padded_participants.div_ceil(8);
let blob_len = round2_package_size(
padded_participants,
network_curve.scalar_bytes_len(),
network_curve.header_bytes_len(),
);
let mut bitmask = vec![0u8; bitmask_len];
let blob = vec![0u8; blob_len];
(0..max_signers).for_each(|i| {
if i != authority_index {
let byte_index = (i / 8) as usize;
let bit_index = (i % 8) as u8;
bitmask[byte_index] |= 1 << bit_index;
}
});
let justification_bundle = BundledPackages { bitmask, blob };
let round4_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_dkg_round4(justification_bundle)
.expect("Round4 package should be valid");
let dkg_package = DkgPackage::Round4(round4_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let justifications = Justifications::<T>::get(&network_curve);
let my_justifications = justifications.get(&authority_index)
.expect("Justifications should be registered");
assert_eq!(my_justifications.count_ones::<AuthIndex>(), max_signers - 1);
Ok(())
}
#[benchmark]
fn register_round5_verifying_package() -> Result<(), BenchmarkError> {
let max_signers = 4 as AuthIndex;
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
(0..max_signers).for_each(|i| {
dkg_state.insert_index(i);
});
let dkg_index = dkg_state.get_dkg_index();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let dummy_merkle_root = ExodusHash::repeat_byte(69);
let dummy_verifying_key = vec![69u8; network_curve.element_bytes_len()];
let round5_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_dkg_round5(dummy_verifying_key.clone(), dummy_merkle_root)
.expect("Round5 package should be valid");
let dkg_package = DkgPackage::Round5(round5_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let bounded_verifying_key =
BoundedVec::<u8, ConstU32< { ELEMENT_MAX_BYTES }>>::try_from(
dummy_verifying_key,
).expect("Bounded verifying key should be correct");
let metadata_hashes = [
SubstrateBlake2Hasher::hash(dummy_merkle_root.as_ref()),
SubstrateBlake2Hasher::hash(bounded_verifying_key.as_ref()),
];
let verifying_hash = sequential_hash::<SubstrateBlake2Hasher, _>(metadata_hashes);
let verification_key = (network_curve, dkg_index, verifying_hash);
let participants = Verifications::<T>::get(verification_key);
assert!(participants.contains(authority_index));
let consensus = VerifyingKeyConsensus::<T>::get(&network_curve, dkg_index);
assert!(consensus.participants.contains(authority_index));
assert_eq!(consensus.participants.count_ones::<AuthIndex>(), 1);
assert_eq!(consensus.merkle_root, dummy_merkle_root);
assert_eq!(consensus.element_bytes, bounded_verifying_key);
Ok(())
}
#[benchmark]
fn register_round6_public_share_package(
c: Linear<1, { T::MaxAuthorities::get().next_power_of_two().trailing_zeros() }>
) -> Result<(), BenchmarkError> {
let computed_max_signers = (1u32 << c) as AuthIndex;
let constant_max_signers = T::MaxAuthorities::get() as AuthIndex;
let max_signers = sp_std::cmp::min(computed_max_signers, constant_max_signers);
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
dkg_state.next_phase();
(0..max_signers).for_each(|i| {
dkg_state.insert_index(i);
});
let dkg_index = dkg_state.get_dkg_index();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
let dummy_value = vec![69u8; network_curve.element_bytes_len()];
let (dummy_merkle_root, dummy_verifying_share, dummy_merkle_proof) =
with_ciphersuite!(network_curve, |f| {
let mut dummy_values = BTreeMap::new();
for i in 0..max_signers {
let non_zero_index = i.saturating_add(1);
let identifier: frost_core::Identifier<Ciphersuite> =
frost_core::Identifier::<Ciphersuite>::try_from(non_zero_index)
.expect("Identifier should be created");
dummy_values.insert(identifier, dummy_value.clone());
}
let merkle_tree =
NetworkCurve::build_merkle_tree(
&dummy_values,
|value: &Vec<u8>| Ok(value.clone()),
).expect("Merkle tree for verifying shares should be valid");
let merkle_root = merkle_tree.last().copied().unwrap();
let (verifying_share, merkle_proof) = NetworkCurve::generate_merkle_proof_from_tree(
&dummy_values,
&merkle_tree,
authority_index,
|value: &Vec<u8>| Ok(value.clone())
).expect("Merkle tree proof should be valid");
(merkle_root, verifying_share, merkle_proof)
});
VerifyingKeyConsensus::<T>::mutate(&network_curve, &dkg_index, |consensus| {
consensus.participants.insert(authority_index);
consensus.merkle_root = dummy_merkle_root;
});
let round6_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_dkg_round6(dummy_verifying_share.clone(), dummy_merkle_proof)
.expect("Round6 package should be valid");
let dkg_package = DkgPackage::Round6(round6_package);
let signature = authority
.sign(&dkg_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, dkg_package, signature);
let participants = VerifyingSharesParticipants::<T>::get(&network_curve, dkg_index);
assert!(participants.contains(authority_index));
let bundled_verifying_share =
BoundedVec::<u8, ConstU32<{ ELEMENT_MAX_BYTES }>>::try_from(
dummy_verifying_share,
).expect("Verifying share should be bundled");
let verifying_share_key = (network_curve, dkg_index, authority_index);
assert_eq!(
VerifyingShares::<T>::get(verifying_share_key).unwrap(),
bundled_verifying_share,
);
Ok(())
}
#[benchmark]
fn register_nonce_commitment() -> Result<(), BenchmarkError> {
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(1);
loop {
if !dkg_state.is_dkg_pending() { break; }
dkg_state.next_phase();
}
dkg_state.insert_index(authority_index);
let exodus_session = CurrentExodus::<T>::get();
let dummy_bytes = EvmBytes32::repeat_byte(69);
let dummy_nonce = vec![0u8; network_curve.element_bytes_len()];
let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0);
let authorities = QualificationAuthorities::<T>::get(&network_curve);
let active_dkg: ActivatedState<T> = (&dkg_state).into();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
let nonce_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_exodus_nonce_commitment(
exodus_session,
dummy_nonce.clone(),
dummy_nonce.clone(),
)
.expect("Nonce commitment package should be valid");
let exodus_package = ExodusPackage::NonceCommitment(nonce_package);
let signature = authority
.sign(&exodus_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, exodus_package, signature);
let roast_session = RoastSessions::<T>::get(&exodus_session, authority_index)
.expect("ROAST session should exist after nonce commitment");
let roast_state = RoastSessionStates::<T>::get(&exodus_session, roast_session);
assert!(roast_state.nonce_committers.contains(authority_index));
assert!(!roast_state.group_committers.contains(authority_index));
assert!(!roast_state.partial_signers.contains(authority_index));
let registered_nonces = NonceCommitments::<T>::get(
(exodus_session, roast_session),
authority_index,
).expect("Nonces should be registered during nonce commitment");
assert_eq!(registered_nonces.hiding, dummy_nonce.clone());
assert_eq!(registered_nonces.binding, dummy_nonce);
Ok(())
}
#[benchmark]
fn register_group_commitment() -> Result<(), BenchmarkError> {
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(1);
loop {
if !dkg_state.is_dkg_pending() { break; }
dkg_state.next_phase();
}
dkg_state.insert_index(authority_index);
let exodus_session = CurrentExodus::<T>::get();
let dummy_bytes = EvmBytes32::repeat_byte(69);
let dummy_merkle_root = ExodusHash::repeat_byte(69);
let dummy_group_commitment = vec![0u8; network_curve.element_bytes_len()];
let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0);
let authorities = QualificationAuthorities::<T>::get(&network_curve);
let active_dkg: ActivatedState<T> = (&dkg_state).into();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
let mut roast_state = RoastSessionState::<ParticipantsBitmap::<T>>::default();
roast_state.status = RoastStatus::GroupCommitments;
roast_state.nonce_committers.insert(authority_index);
let roast_session = 3;
RoastSessions::<T>::insert(&exodus_session, authority_index, roast_session);
RoastSessionStates::<T>::insert(exodus_session, roast_session, roast_state);
let group_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_exodus_group_commitment(
exodus_session,
dummy_group_commitment.clone(),
dummy_bytes,
)
.expect("Group commitment package should be valid");
let exodus_package = ExodusPackage::GroupCommitment(group_package);
let signature = authority
.sign(&exodus_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, exodus_package, signature);
let bounded_group_commitment =
BoundedVec::<u8, ConstU32<{ ELEMENT_MAX_BYTES }>>::try_from(
dummy_group_commitment,
).expect("Group commtiment should be valid");
let roast_state = RoastSessionStates::<T>::get(&exodus_session, roast_session);
assert!(roast_state.nonce_committers.contains(authority_index));
assert!(roast_state.group_committers.contains(authority_index));
let consensus = GroupCommitmentsConsensus::<T>::get(exodus_session, roast_session);
assert!(consensus.participants.contains(authority_index));
assert_eq!(&consensus.element_bytes, &bounded_group_commitment);
assert_eq!(consensus.merkle_root, dummy_merkle_root);
let group_commitment_key = (exodus_session, roast_session, dummy_merkle_root, bounded_group_commitment);
let participants = GroupCommitters::<T>::get(group_commitment_key);
assert!(participants.contains(authority_index));
Ok(())
}
#[benchmark]
fn register_signature_share() -> Result<(), BenchmarkError> {
let max_signers = T::MaxAuthorities::get();
let threshold = get_byzantium_threshold(max_signers);
let (mut dkg_state, network_curve, authority, authority_index) =
prepare_pallet::<T>(max_signers as usize);
loop {
if !dkg_state.is_dkg_pending() { break; }
dkg_state.next_phase();
}
dkg_state.insert_index(authority_index);
let dkg_index = dkg_state.get_dkg_index();
let exodus_session = CurrentExodus::<T>::get();
let mut dummy_element = vec![0u8; network_curve.element_bytes_len()];
dummy_element[0] = 0x02;
let g_x_coords = [
0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac,
0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, 0x07,
0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9,
0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98
];
dummy_element[1..33].copy_from_slice(&g_x_coords);
let dummy_bytes = EvmBytes32::repeat_byte(69);
let request = ExodusRequest::evm_rotation(exodus_session, dummy_bytes, 0);
let mut dummy_scalar = vec![0u8; network_curve.scalar_bytes_len()];
dummy_scalar[network_curve.scalar_bytes_len() - 1] = 1;
let dummy_signature_share = dummy_scalar.clone();
let dummy_binding_factor = dummy_scalar.clone();
let dummy_group_commitment = dummy_element.clone();
let (dummy_merkle_root, dummy_merkle_proof) =
with_ciphersuite!(network_curve, |f| {
let mut dummy_values = BTreeMap::new();
for i in 0..threshold {
let non_zero_index = (i as AuthIndex).saturating_add(1);
let identifier: frost_core::Identifier<Ciphersuite> =
frost_core::Identifier::<Ciphersuite>::try_from(non_zero_index)
.expect("Identifier should be created");
dummy_values.insert(identifier, dummy_binding_factor.clone());
}
let merkle_tree =
NetworkCurve::build_merkle_tree(
&dummy_values,
|value: &Vec<u8>| Ok(value.clone()),
).expect("Merkle tree for binding factor should be valid");
let merkle_root = merkle_tree.last().copied().unwrap();
let (_, merkle_proof) =
NetworkCurve::generate_merkle_proof_from_tree(
&dummy_values,
&merkle_tree,
authority_index,
|value: &Vec<u8>| Ok(value.clone())
).expect("Merkle tree proof should be valid");
(merkle_root, merkle_proof)
});
let authorities = QualificationAuthorities::<T>::get(&network_curve);
let active_dkg: ActivatedState<T> = (&dkg_state).into();
let active_dkg_index = active_dkg.get_dkg_index();
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
ActiveAuthorities::<T>::insert(&network_curve, authorities);
ExodusRequests::<T>::insert(network_curve, exodus_session, request);
let mut roast_state = RoastSessionState::<ParticipantsBitmap::<T>>::default();
roast_state.status = RoastStatus::PartialSignatures;
roast_state.nonce_committers.insert(authority_index);
roast_state.group_committers.insert(authority_index);
(0..threshold).for_each(|i| {
if i as AuthIndex == authority_index { return; }
roast_state.partial_signers.insert(i);
});
let roast_session = 3;
RoastSessions::<T>::insert(&exodus_session, authority_index, roast_session);
RoastSessionStates::<T>::insert(exodus_session, roast_session, roast_state);
let bounded_group_commitment = BoundedVec::try_from(dummy_group_commitment).unwrap();
let dummy_verifying_key = BoundedVec::try_from(dummy_element.clone()).unwrap();
let dummy_verifying_share = BoundedVec::try_from(dummy_element.clone()).unwrap();
let dummy_nonce = ExodusSeparatedNonce::new(
BoundedVec::try_from(dummy_element.clone()).unwrap(),
BoundedVec::try_from(dummy_element.clone()).unwrap(),
);
ActiveVerifyingKey::<T>::insert(&network_curve, dummy_verifying_key);
VerifyingShares::<T>::insert((&network_curve, active_dkg_index, authority_index), dummy_verifying_share);
NonceCommitments::<T>::insert((&exodus_session, &roast_session), authority_index, dummy_nonce);
GroupCommitmentsConsensus::<T>::mutate(&exodus_session, &roast_session, |consensus| {
consensus.element_bytes = bounded_group_commitment.clone();
consensus.merkle_root = dummy_merkle_root;
(0..threshold).for_each(|i| {
consensus.participants.insert(i);
});
});
let share_package = PackageContext::default()
.with_network_curve(NetworkCurve::Secp256k1)
.with_authority_index(authority_index)
.with_exodus_signature_share(
exodus_session,
dummy_signature_share.clone(),
dummy_merkle_proof,
dummy_binding_factor
)
.expect("Signature share package should be valid");
let exodus_package = ExodusPackage::SignatureShare(share_package);
let signature = authority
.sign(&exodus_package.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, exodus_package, signature);
let maybe_roast_session = RoastSessions::<T>::get(&exodus_session, authority_index);
assert!(maybe_roast_session.is_none());
let scalar_exists = SignatureScalars::<T>::contains_key(&exodus_session, &roast_session);
let consensus_exists = GroupCommitmentsConsensus::<T>::contains_key(&exodus_session, &roast_session);
let committers_exists = GroupCommitters::<T>::contains_key((
&exodus_session,
&roast_session,
&dummy_merkle_root,
&bounded_group_commitment,
));
assert!(!scalar_exists);
assert!(!consensus_exists);
assert!(!committers_exists);
assert!(!ExodusRequests::<T>::contains_key(&network_curve, &exodus_session));
assert!(ExodusSignedRotations::<T>::contains_key((&exodus_session, NetworkType::Evm), dkg_index));
Ok(())
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext_benchmarks(),
crate::mock::Runtime
);
}

View File

@ -1,94 +0,0 @@
use ghost_helpers::networks::NetworkCurve;
use crate::AuthIndex;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ExodusError {
InvalidParticipantId,
DeserializationError,
SerializationError,
InvalidMinSigners(AuthIndex, AuthIndex),
InvalidMaxSigners(AuthIndex),
DKGNotSupported(NetworkCurve),
IncorrectNumberOfCoefficients,
IncorrectNumberOfCommitments,
IncorrectNumberOfPackages,
IncorrectPackage,
PackageNotFound,
InvalidSecretShare,
InvalidProofOfKnowledge,
MissingCommitment,
IncorrectCommitment,
UnknownIdentifier,
IdentityCommitment,
DuplicatedIdentifier,
IncorrectNumberOfIdentifiers,
InvalidSignature,
InvalidSignatureShare,
InvalidNonceCommitments,
EncryptionFailed,
DecryptionFailed,
HKDFFailed,
InvalidNonceLength,
MalformedSigningKey,
NoStoredNetworks,
OffchainTimeoutPeriod,
BadSignature(AuthIndex),
TransactionSubmissionFailed(AuthIndex),
UnknownReceiverIndex(AuthIndex),
CouldNotConstructMessage(AuthIndex),
NothingToWrite,
UnknownPrefix,
UnexpectedRound,
InvalidMerkleProof,
InvalidIdentityElement,
NoActiveAuthorities,
Unknown,
}
impl core::fmt::Debug for ExodusError {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
ExodusError::InvalidParticipantId => write!(fmt, "Participant index could not be parsed to identifier."),
ExodusError::DeserializationError => write!(fmt, "Error during deserializing value."),
ExodusError::SerializationError => write!(fmt, "Error serializing value."),
ExodusError::InvalidMinSigners(min, max) => write!(fmt, "Number of min_signers ({min}) must be at least 2 and not larger than max_signers ({max})."),
ExodusError::InvalidMaxSigners(max_signers) => write!(fmt, "Number of max_signers must be at least 2, {max_signers} given."),
ExodusError::DKGNotSupported(ref curve) => write!(fmt, "The ciphersuite {curve:?} does not support DKG."),
ExodusError::IncorrectNumberOfCoefficients => write!(fmt, "Incorrect number of coefficients; should be equal to min signers."),
ExodusError::IncorrectNumberOfCommitments => write!(fmt, "Incorrect number of commitments; should be equal to min signers."),
ExodusError::InvalidNonceCommitments => write!(fmt, "Hiding nonce commitments indexes should match binding nonce commitments."),
ExodusError::InvalidProofOfKnowledge => write!(fmt, "The proof of knowledge is not valid."),
ExodusError::IncorrectNumberOfPackages => write!(fmt, "Incorrect number of packages; should be equal to max signers."),
ExodusError::IncorrectPackage => write!(fmt, "The incorrect package was specified."),
ExodusError::PackageNotFound => write!(fmt, "Round 1 package not found for Round 2 participant."),
ExodusError::InvalidSecretShare => write!(fmt, "Invalid secret share."),
ExodusError::MissingCommitment => write!(fmt, "The Signing Package must contain the participant's Commitments."),
ExodusError::IncorrectCommitment => write!(fmt, "The participant's commitment is incorrect."),
ExodusError::UnknownIdentifier => write!(fmt, "Unknown identifier."),
ExodusError::IdentityCommitment => write!(fmt, "Commitment equals the identity."),
ExodusError::DuplicatedIdentifier => write!(fmt, "Duplicated identifier."),
ExodusError::IncorrectNumberOfIdentifiers => write!(fmt, "Incorrect number of identifiers."),
ExodusError::InvalidSignature => write!(fmt, "Invalid signature."),
ExodusError::InvalidSignatureShare => write!(fmt, "Invalid signature share."),
ExodusError::EncryptionFailed => write!(fmt, "Encryption failed."),
ExodusError::DecryptionFailed => write!(fmt, "Decryption failed."),
ExodusError::HKDFFailed => write!(fmt, "HKDF failed."),
ExodusError::InvalidNonceLength => write!(fmt, "Encryption nonce has invalid length."),
ExodusError::MalformedSigningKey => write!(fmt, "Malformed signing key encoding."),
ExodusError::NoStoredNetworks => write!(fmt, "No stored networks to be worked on."),
ExodusError::OffchainTimeoutPeriod => write!(fmt, "Offchain request should be in-flight."),
ExodusError::BadSignature(auth_index) => write!(fmt, "Signature could not be created from {auth_index}."),
ExodusError::TransactionSubmissionFailed(auth_index) => write!(fmt, "Could not submit transaction from {auth_index}."),
ExodusError::UnknownReceiverIndex(receiver_index) => write!(fmt, "Unknown receiver index {receiver_index} found."),
ExodusError::CouldNotConstructMessage(auth_index) => write!(fmt, "Could not construct message during OCW from {auth_index}."),
ExodusError::NothingToWrite => write!(fmt, "Nothing to write into local storage."),
ExodusError::UnknownPrefix => write!(fmt, "Unknown prefix used for local storage."),
ExodusError::UnexpectedRound => write!(fmt, "Unexpected round provided."),
ExodusError::InvalidMerkleProof => write!(fmt, "Invalid merkle proof provided."),
ExodusError::InvalidIdentityElement => write!(fmt, "Invalid identity element provided."),
ExodusError::NoActiveAuthorities => write!(fmt, "No active authorities for signing exodus transactions."),
ExodusError::Unknown => write!(fmt, "Unknown error."),
}
}
}

View File

@ -1,496 +0,0 @@
use crate::{EncryptedMessage, FrostError};
use ghost_networks::NetworkCurve;
use chacha20poly1305::{
aead::{self, Aead, AeadCore, KeyInit},
Key as EncryptionKey,
Nonce as EncryptionNonce,
ChaCha20Poly1305 as EncryptionCipher,
};
use hkdf::Hkdf as KeyDerivation;
use sha2::{Digest, Sha256 as Hashing};
use rand_chacha::rand_core::{CryptoRng, RngCore};
use sp_std::collections::btree_map::BTreeMap;
macro_rules! with_ciphersuite {
($curve:expr, |$alias:ident| $body:block) => {
match $curve {
ExodusCurve::Secp256k1 => {
use frost_secp256k1 as $alias;
$body
},
NetworkCurve::Ed25519 => {
use frost_ed25519 as $alias;
$body
}
}
}
}
impl ExodusCurve {
pub fn dkg_verify_proof_of_knowledge(
&self,
index: u16,
round1_package_bytes: &[u8],
) -> Result<(), FrostError> {
with_ciphersuite!(self, |f| {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
frost_core::keys::dkg::verify_proof_of_knowledge(
identifier,
&round1_package.commitment(),
&round1_package.proof_of_knowledge(),
).map_err(|_| FrostError::InvalidProofOfKnowledge)
})
}
pub fn verify_signature_share(
&self,
index: u16,
verifying_share_bytes: &[u8],
signature_share_bytes: &[u8],
signing_package_bytes: &[u8],
verifying_key_bytes: &[u8],
) -> Result<(), FrostError> {
with_ciphersuite!(self, |f| {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let verifying_share = f::keys::VerifyingShare::deserialize(verifying_share_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signature_share = f::round2::SignatureShare::deserialize(signature_share_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signing_package = f::SigningPackage::deserialize(signing_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let verifying_key = f::VerifyingKey::deserialize(verifying_key_bytes)
.map_err(|_| FrostError::DeserializationError)?;
frost_core::verify_signature_share(
identifier,
&verifying_share,
&signature_share,
&signing_package,
&verifying_key,
).map_err(|err| match err {
f::Error::IdentityCommitment => FrostError::IdentityCommitment,
f::Error::UnknownIdentifier => FrostError::UnknownIdentifier,
f::Error::DuplicatedIdentifier => FrostError::DuplicatedIdentifier,
f::Error::InvalidSignatureShare { .. } => FrostError::InvalidSignatureShare,
_ => FrostError::Unknown,
})
})
}
pub fn aggregate_signature(
&self,
signing_package_bytes: &[u8],
signature_shares_bytes: &BTreeMap<u16, Vec<u8>>,
pubkeys_bytes: &[u8],
) -> Result<Vec<u8>, FrostError> {
with_ciphersuite!(self, |f| {
let signing_package = f::SigningPackage::deserialize(signing_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let mut signature_shares = BTreeMap::new();
for (&index, signature_share_bytes) in signature_shares_bytes.iter() {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let signature_share = f::round2::SignatureShare::deserialize(signature_share_bytes)
.map_err(|_| FrostError::DeserializationError)?;
signature_shares.insert(identifier, signature_share);
}
let pubkeys = f::keys::PublicKeyPackage::deserialize(pubkeys_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signature = f::aggregate(&signing_package, &signature_shares, &pubkeys)
.map_err(|err| match err {
f::Error::UnknownIdentifier => FrostError::UnknownIdentifier,
f::Error::SerializationError => FrostError::SerializationError,
f::Error::IdentityCommitment => FrostError::IdentityCommitment,
f::Error::IncorrectNumberOfIdentifiers => FrostError::IncorrectNumberOfIdentifiers,
f::Error::DuplicatedIdentifier => FrostError::DuplicatedIdentifier,
f::Error::InvalidSecretShare { .. } => FrostError::InvalidSecretShare,
f::Error::InvalidSignature { .. } => FrostError::InvalidSignature,
_ => FrostError::Unknown,
})?;
let signature_bytes = signature.serialize()
.map_err(|_| FrostError::SerializationError)?;
Ok(signature_bytes)
})
}
pub fn is_signature_valid(
&self,
pubkey_bytes: &[u8],
signature_bytes: &[u8],
message: &[u8],
) -> bool {
with_ciphersuite!(self, |f| {
let get_verification_result = || -> Result<(), FrostError> {
let pubkeys = f::keys::PublicKeyPackage::deserialize(pubkey_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signature = f::Signature::deserialize(signature_bytes)
.map_err(|_| FrostError::DeserializationError)?;
pubkeys.verifying_key()
.verify(message, &signature)
.map_err(|_| FrostError::InvalidSignature)?;
Ok(())
};
get_verification_result().is_ok()
})
}
pub fn dkg_part1<R: RngCore + CryptoRng>(
&self,
index: u16,
max_signers: u16,
min_signers: u16,
mut rng: R,
) -> Result<(Vec<u8>, Vec<u8>), FrostError> {
with_ciphersuite!(self, |f| {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let (round1_secret_package, round1_package) = f::keys::dkg::part1(
identifier,
max_signers,
min_signers,
&mut rng,
).map_err(|err| {
match err {
f::Error::InvalidMinSigners => FrostError::InvalidMinSigners(min_signers, max_signers),
f::Error::InvalidMaxSigners => FrostError::InvalidMaxSigners(max_signers),
f::Error::DKGNotSupported => FrostError::DKGNotSupported(*self),
_ => FrostError::Unknown,
}
})?;
let round1_secret_package_bytes = round1_secret_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
let round1_package_bytes = round1_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
Ok((round1_secret_package_bytes, round1_package_bytes))
})
}
fn convert_identifier_to_index(
identifier_bytes: Vec<u8>
) -> Result<u16, FrostError> {
const SIZE: usize = core::mem::size_of::<u16>();
let start = identifier_bytes.len().checked_sub(SIZE)
.ok_or(FrostError::InvalidParticipantId)?;
let bytes_array = identifier_bytes.get(start..)
.and_then(|slice| slice.try_into().ok())
.ok_or(FrostError::InvalidParticipantId)?;
Ok(u16::from_be_bytes(bytes_array))
}
pub fn dkg_part2(
&self,
round1_secret_package_bytes: &[u8],
round1_packages_bytes: &BTreeMap<u16, Vec<u8>>,
) -> Result<(Vec<u8>, BTreeMap<u16, Vec<u8>>), FrostError> {
with_ciphersuite!(self, |f| {
let round1_secret_package = f::keys::dkg::round1::SecretPackage::deserialize(round1_secret_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let mut round1_packages = BTreeMap::new();
for (&index, round1_package_bytes) in round1_packages_bytes.iter() {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
round1_packages.insert(identifier, round1_package);
}
let (round2_secret_package, round2_packages) = f::keys::dkg::part2(
round1_secret_package,
&round1_packages,
).map_err(|err| match err {
f::Error::IncorrectNumberOfCommitments => FrostError::IncorrectNumberOfCommitments,
f::Error::IncorrectNumberOfPackages => FrostError::IncorrectNumberOfPackages,
f::Error::InvalidProofOfKnowledge { .. } => FrostError::InvalidProofOfKnowledge,
_ => FrostError::Unknown,
})?;
let round2_secret_package_bytes = round2_secret_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
let mut round2_packages_bytes = BTreeMap::new();
for (participant_identifier, round2_package) in round2_packages.iter() {
let index = Self::convert_identifier_to_index(
participant_identifier.serialize(),
)?;
let round2_package_bytes = round2_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
round2_packages_bytes.insert(index as u16, round2_package_bytes);
}
Ok((round2_secret_package_bytes, round2_packages_bytes))
})
}
pub fn dkg_encrypt_round2_package<R: RngCore + CryptoRng>(
&self,
cipher: &EncryptionCipher,
associated_data: &[u8],
round2_package_bytes: &[u8],
mut rng: R,
) -> Result<(Vec<u8>, Vec<u8>), FrostError> {
let payload = aead::Payload {
msg: round2_package_bytes,
aad: associated_data,
};
let nonce = EncryptionCipher::generate_nonce(&mut rng);
let ciphertext = cipher.encrypt(&nonce, payload)
.map_err(|_| FrostError::EncryptionFailed)?;
Ok((ciphertext, nonce.to_vec()))
}
pub fn dkg_decrypt_round2_package(
&self,
cipher: &EncryptionCipher,
encrypted_message: &EncryptedMessage,
associated_data: &[u8],
) -> Result<Vec<u8>, FrostError> {
let nonce = EncryptionNonce::from_iter(encrypted_message.nonce.iter().cloned());
let payload = aead::Payload {
msg: &encrypted_message.ciphertext,
aad: associated_data,
};
cipher.decrypt(&nonce, payload)
.map_err(|_| FrostError::DecryptionFailed)
}
pub fn prepare_additional_info(
&self,
sender_index: u16,
receiver_index: u16,
session_index: u16,
) -> Vec<u8> {
let mut additional_info = match self {
ExodusCurve::Secp256k1 => b"EXODUS-SECP256K1-DKG-V1".to_vec(),
};
additional_info.extend_from_slice(&sender_index.to_be_bytes());
additional_info.extend_from_slice(&receiver_index.to_be_bytes());
additional_info.extend_from_slice(&session_index.to_be_bytes());
additional_info
}
pub fn prepare_cipher_from_keys(
&self,
round1_receiver_package_bytes: &[u8],
round1_secret_package_bytes: &[u8],
additional_info: &[u8],
) -> Result<EncryptionCipher, FrostError> {
with_ciphersuite!(self, |f| {
let coefficients =
f::keys::dkg::round1::SecretPackage::deserialize(round1_secret_package_bytes)
.map_err(|_| FrostError::DeserializationError)?
.coefficients();
let sender_secret_key = coefficients
.first()
.ok_or(FrostError::IncorrectNumberOfCoefficients)?;
let receiver_public_key =
f::keys::dkg::round1::Package::deserialize(round1_receiver_package_bytes)
.map_err(|_| FrostError::DeserializationError)?
.commitment()
.coefficients()
.first()
.ok_or(FrostError::MissingCommitment)?
.value();
let shared_secret_bytes = f::VerifyingKey::new(receiver_public_key * sender_secret_key)
.serialize()
.map_err(|_| FrostError::SerializationError)?;
let salt = Hashing::digest(&additional_info);
let hk = KeyDerivation::<Hashing>::new(Some(&salt), &shared_secret_bytes);
let mut encryption_key = EncryptionKey::default();
hk.expand(additional_info, &mut encryption_key)
.map_err(|_| FrostError::HKDFFailed)?;
let cipher = EncryptionCipher::new_from_slice(&encryption_key)
.expect("could not happen; length is already correct qed");
Ok(cipher)
})
}
pub fn dkg_verify_private_package(
&self,
index: u16,
round1_package_bytes: &[u8],
round2_package_bytes: &[u8],
) -> Result<(), FrostError> {
with_ciphersuite!(self, |f| {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let commitment = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map(|round1_package| round1_package.commitment().clone())
.map_err(|_| FrostError::DeserializationError)?;
let secret_share = f::keys::dkg::round2::Package::deserialize(round2_package_bytes)
.map(|round2_package| {
let signing_share = round2_package.signing_share().clone();
f::keys::SecretShare::new(identifier, signing_share, commitment)
})
.map_err(|_| FrostError::DeserializationError)?;
let _ = secret_share.verify().map_err(|_| FrostError::InvalidSecretShare)?;
Ok(())
})
}
pub fn dkg_part3(
&self,
round2_secret_package_bytes: &[u8],
round1_packages_bytes: &BTreeMap<u16, Vec<u8>>,
round2_packages_bytes: &BTreeMap<u16, Vec<u8>>,
) -> Result<(Vec<u8>, Vec<u8>), FrostError> {
with_ciphersuite!(self, |f| {
let round2_secret_package = f::keys::dkg::round2::SecretPackage::deserialize(round2_secret_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let mut round1_packages = BTreeMap::new();
for (&index, round1_package_bytes) in round1_packages_bytes.iter() {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
round1_packages.insert(identifier, round1_package);
}
let mut round2_packages = BTreeMap::new();
for (&index, round2_package_bytes) in round2_packages_bytes.iter() {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let round2_package = f::keys::dkg::round2::Package::deserialize(round2_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
round2_packages.insert(identifier, round2_package);
}
let (key_package, pubkey_package) = f::keys::dkg::part3(
&round2_secret_package,
&round1_packages,
&round2_packages,
).map_err(|err| match err {
f::Error::IncorrectNumberOfPackages => FrostError::IncorrectNumberOfPackages,
f::Error::IncorrectPackage => FrostError::IncorrectPackage,
f::Error::PackageNotFound => FrostError::PackageNotFound,
f::Error::InvalidSecretShare { .. } => FrostError::InvalidSecretShare,
f::Error::IncorrectNumberOfCommitments => FrostError::IncorrectNumberOfCommitments,
_ => FrostError::Unknown,
})?;
let key_package_bytes = key_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
let pubkey_package_bytes = pubkey_package.serialize()
.map_err(|_| FrostError::SerializationError)?;
Ok((key_package_bytes, pubkey_package_bytes))
})
}
pub fn generate_nonce<R: CryptoRng + RngCore>(
&self,
secret_key_package_bytes: &[u8],
mut rng: R
) -> Result<(Vec<u8>, Vec<u8>), FrostError> {
with_ciphersuite!(self, |f| {
let signing_share = f::keys::KeyPackage::deserialize(secret_key_package_bytes)
.map(|key_package| *key_package.signing_share())
.map_err(|_| FrostError::DeserializationError)?;
let (signing_nonces, signing_commitments) =
f::round1::commit(&signing_share, &mut rng);
let signing_nonces_bytes = signing_nonces.serialize()
.map_err(|_| FrostError::SerializationError)?;
let signing_commitments_bytes = signing_commitments.serialize()
.map_err(|_| FrostError::SerializationError)?;
Ok((signing_nonces_bytes, signing_commitments_bytes))
})
}
pub fn generate_signing_package(
&self,
commitments_map_bytes: &BTreeMap<u16, Vec<u8>>,
message: &[u8],
) -> Result<Vec<u8>, FrostError> {
with_ciphersuite!(self, |f| {
let mut commitments_map = BTreeMap::new();
for (&index, commitment_bytes) in commitments_map_bytes.iter() {
let identifier = index.try_into().map_err(|_| FrostError::InvalidParticipantId)?;
let commitment = f::round1::SigningCommitments::deserialize(commitment_bytes)
.map_err(|_| FrostError::DeserializationError)?;
commitments_map.insert(identifier, commitment);
}
f::SigningPackage::new(commitments_map, message)
.serialize()
.map_err(|_| FrostError::SerializationError)
})
}
pub fn sign_message(
&self,
signing_package_bytes: &[u8],
signer_nonces_bytes: &[u8],
secret_key_share_bytes: &[u8]
) -> Result<Vec<u8>, FrostError> {
with_ciphersuite!(self, |f| {
let signing_package = f::SigningPackage::deserialize(signing_package_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signer_nonces = f::round1::SigningNonces::deserialize(signer_nonces_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let secret_key_share = f::keys::KeyPackage::deserialize(secret_key_share_bytes)
.map_err(|_| FrostError::DeserializationError)?;
let signature_share_bytes = f::round2::sign(&signing_package, &signer_nonces, &secret_key_share)
.map(|signature_share| signature_share.serialize())
.map_err(|err| match err {
f::Error::IncorrectNumberOfCommitments => FrostError::IncorrectNumberOfCommitments,
f::Error::MissingCommitment => FrostError::MissingCommitment,
f::Error::IncorrectCommitment => FrostError::IncorrectCommitment,
f::Error::SerializationError => FrostError::SerializationError,
f::Error::UnknownIdentifier => FrostError::UnknownIdentifier,
f::Error::IdentityCommitment => FrostError::IdentityCommitment,
f::Error::DuplicatedIdentifier => FrostError::DuplicatedIdentifier,
f::Error::IncorrectNumberOfIdentifiers => FrostError::IncorrectNumberOfIdentifiers,
_ => FrostError::Unknown,
})?;
Ok(signature_share_bytes)
})
}
}

View File

@ -1,26 +0,0 @@
use crate::{AuthIndex, ExodusError, NetworkCurve};
use ghost_traits::exodus::IdentifierConverter;
impl IdentifierConverter<AuthIndex, ExodusError> for NetworkCurve {
fn convert_identifier_to_index(
identifier_bytes: &[u8],
) -> Result<AuthIndex, ExodusError> {
const SIZE: usize = core::mem::size_of::<AuthIndex>();
let start = identifier_bytes.len().checked_sub(SIZE)
.ok_or(ExodusError::InvalidParticipantId)?;
let bytes_array = identifier_bytes.get(start..)
.and_then(|slice| slice.try_into().ok())
.ok_or(ExodusError::InvalidParticipantId)?;
AuthIndex::from_be_bytes(bytes_array)
.checked_sub(1)
.ok_or(ExodusError::InvalidParticipantId)
}
fn non_zero_index(index: AuthIndex) -> Result<AuthIndex, ExodusError> {
index.checked_add(1).ok_or(ExodusError::InvalidParticipantId)
}
}

View File

@ -1,351 +0,0 @@
use sp_std::{collections::btree_map::BTreeMap, vec::Vec, result::Result};
use rand_chacha::rand_core::{CryptoRng, RngCore};
use ghost_traits::exodus::{
DistributedKeyGeneration, IdentifierConverter, MerkleTreeBuilder,
};
use crate::{AuthIndex, ExodusError, NetworkCurve};
impl DistributedKeyGeneration<AuthIndex, ExodusError> for NetworkCurve {
type RoundPackages = BTreeMap<AuthIndex, Vec<u8>>;
type Part1Result = Result<(Vec<u8>, Vec<u8>), ExodusError>;
type Part2Result = Result<(Vec<u8>, BTreeMap<AuthIndex, Vec<u8>>), ExodusError>;
type Part3Result = Result<
(
Vec<u8>, Vec<u8>,
Vec<<Self as MerkleTreeBuilder<AuthIndex, ExodusError>>::Hash>,
<Self as MerkleTreeBuilder<AuthIndex, ExodusError>>::Hash,
),
ExodusError
>;
fn dkg_narrow_round_max_signers(
&self,
secret_package_bytes: &[u8],
new_max_signers: AuthIndex,
round_number: u8,
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
macro_rules! process_secret_package {
(
$package_type:ty,
$field1:ident : $action1:tt,
$field2:ident : $action2:tt,
) => {{
let secret_package = <$package_type>::deserialize(secret_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let min_signers = *secret_package.min_signers();
frost_core::keys::validate_num_of_signers(min_signers, new_max_signers)
.map_err(|err| match err {
f::Error::InvalidMinSigners => {
ExodusError::InvalidMinSigners(min_signers, new_max_signers)
},
f::Error::InvalidMaxSigners => {
ExodusError::InvalidMaxSigners(new_max_signers)
},
_ => ExodusError::Unknown,
})?;
macro_rules! apply_action {
($field:ident, clone) => { secret_package.$field().clone() };
($field:ident, value) => { secret_package.$field() };
}
let new_secret_package = <$package_type>::new(
secret_package.identifier().clone(),
apply_action!($field1, $action1),
apply_action!($field2, $action2),
min_signers,
new_max_signers,
);
new_secret_package.serialize()
.map_err(|_| ExodusError::SerializationError)
}};
}
match round_number {
1 => process_secret_package!(
f::keys::dkg::round1::SecretPackage,
coefficients: value,
commitment: clone,
),
2 => process_secret_package!(
f::keys::dkg::round2::SecretPackage,
commitment: clone,
secret_share: value,
),
_ => Err(ExodusError::UnexpectedRound),
}
})
}
fn dkg_verify_proof_of_knowledge(
&self,
index: AuthIndex,
round1_package_bytes: &[u8],
) -> Result<AuthIndex, ExodusError> {
with_ciphersuite!(self, |f| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let commitment = round1_package.commitment();
frost_core::keys::dkg::verify_proof_of_knowledge(
identifier,
&commitment,
&round1_package.proof_of_knowledge(),
).map_err(|_| ExodusError::InvalidProofOfKnowledge)?;
let coefficients = commitment.coefficients();
let coefficients_len = coefficients.len() as AuthIndex;
Ok(coefficients_len)
})
}
fn dkg_part1<R: RngCore + CryptoRng>(
&self,
index: AuthIndex,
max_signers: AuthIndex,
min_signers: AuthIndex,
mut rng: R,
) -> Self::Part1Result {
with_ciphersuite!(self, |f| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let (round1_secret_package, round1_package) = f::keys::dkg::part1(
identifier,
max_signers,
min_signers,
&mut rng,
).map_err(|err| {
match err {
f::Error::InvalidMinSigners => ExodusError::InvalidMinSigners(min_signers, max_signers),
f::Error::InvalidMaxSigners => ExodusError::InvalidMaxSigners(max_signers),
f::Error::DKGNotSupported => ExodusError::DKGNotSupported(*self),
_ => ExodusError::Unknown,
}
})?;
let round1_secret_package_bytes = round1_secret_package.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let round1_package_bytes = round1_package.serialize()
.map_err(|_| ExodusError::SerializationError)?;
Ok((round1_secret_package_bytes, round1_package_bytes))
})
}
fn dkg_part2(
&self,
round1_secret_package_bytes: &[u8],
round1_packages_bytes: &Self::RoundPackages,
) -> Self::Part2Result {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
type InnerField = <InnerGroup as frost_core::Group>::Field;
let round1_secret_package = f::keys::dkg::round1::SecretPackage::deserialize(round1_secret_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let round1_packages = round1_packages_bytes
.iter()
.map(|(&index, round1_package_bytes)| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
Ok((identifier, round1_package))
})
.collect::<Result<BTreeMap<f::Identifier, f::keys::dkg::round1::Package>, ExodusError>>()?;
let (round2_secret_package, round2_packages) = f::keys::dkg::part2(
round1_secret_package,
&round1_packages,
).map_err(|err| match err {
f::Error::IncorrectNumberOfCommitments => ExodusError::IncorrectNumberOfCommitments,
f::Error::IncorrectNumberOfPackages => ExodusError::IncorrectNumberOfPackages,
f::Error::InvalidProofOfKnowledge { .. } => ExodusError::InvalidProofOfKnowledge,
_ => ExodusError::Unknown,
})?;
let round2_secret_package_bytes = round2_secret_package.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let round2_packages_bytes = round2_packages
.iter()
.map(|(id, round2_package)| {
let id_scalar = id.to_scalar();
let id_bytes = <InnerField as frost_core::Field>::serialize(&id_scalar);
let index = Self::convert_identifier_to_index(id_bytes.as_ref())?;
let round2_package_bytes = round2_package.serialize()
.map_err(|_| ExodusError::SerializationError)?;
Ok((index, round2_package_bytes))
})
.collect::<Result<BTreeMap<AuthIndex, Vec<u8>>, ExodusError>>()?;
Ok((round2_secret_package_bytes, round2_packages_bytes))
})
}
fn dkg_verify_private_package(
&self,
index: AuthIndex,
round1_package_bytes: &[u8],
round2_package_bytes: &[u8],
) -> Result<(), ExodusError> {
with_ciphersuite!(self, |f| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let commitment = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map(|round1_package| round1_package.commitment().clone())
.map_err(|_| ExodusError::DeserializationError)?;
let secret_share = f::keys::dkg::round2::Package::deserialize(round2_package_bytes)
.map(|round2_package| {
let signing_share = round2_package.signing_share().clone();
f::keys::SecretShare::new(identifier, signing_share, commitment)
})
.map_err(|_| ExodusError::DeserializationError)?;
let _ = secret_share.verify().map_err(|_| ExodusError::InvalidSecretShare)?;
Ok(())
})
}
fn dkg_part3(
&self,
authority_index: AuthIndex,
round2_secret_package_bytes: &[u8],
round1_packages_bytes: &Self::RoundPackages,
round2_packages_bytes: &Self::RoundPackages,
) -> Self::Part3Result {
with_ciphersuite!(self, |f| {
let round2_secret_package = f::keys::dkg::round2::SecretPackage::deserialize(round2_secret_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let round1_packages = round1_packages_bytes
.iter()
.map(|(&index, round1_package_bytes)| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let round1_package = f::keys::dkg::round1::Package::deserialize(round1_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
Ok((identifier, round1_package))
})
.collect::<Result<BTreeMap<f::Identifier, f::keys::dkg::round1::Package>, ExodusError>>()?;
let round2_packages = round2_packages_bytes
.iter()
.map(|(&index, round2_package_bytes)| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let round2_package = f::keys::dkg::round2::Package::deserialize(round2_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
Ok((identifier, round2_package))
})
.collect::<Result<BTreeMap<f::Identifier, f::keys::dkg::round2::Package>, ExodusError>>()?;
let (key_package, pubkey_package) = f::keys::dkg::part3(
&round2_secret_package,
&round1_packages,
&round2_packages,
).map_err(|err| match err {
f::Error::IncorrectNumberOfPackages => ExodusError::IncorrectNumberOfPackages,
f::Error::IncorrectPackage => ExodusError::IncorrectPackage,
f::Error::PackageNotFound => ExodusError::PackageNotFound,
f::Error::InvalidSecretShare { .. } => ExodusError::InvalidSecretShare,
f::Error::IncorrectNumberOfCommitments => ExodusError::IncorrectNumberOfCommitments,
_ => ExodusError::Unknown,
})?;
let key_package_bytes = key_package.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let verifying_shares_merkle_tree =
Self::build_merkle_tree(
pubkey_package.verifying_shares(),
|verifying_share| verifying_share.serialize()
.map_err(|_| ExodusError::SerializationError)
)?;
let (_, merkle_proof) =
Self::generate_merkle_proof_from_tree(
pubkey_package.verifying_shares(),
&verifying_shares_merkle_tree,
authority_index,
|verifying_share| verifying_share.serialize()
.map_err(|_| ExodusError::SerializationError)
)?;
let merkle_root = verifying_shares_merkle_tree
.last()
.copied()
.ok_or(ExodusError::IncorrectNumberOfPackages)?;
let verifying_key_bytes = pubkey_package.verifying_key()
.serialize()
.map_err(|_| ExodusError::SerializationError)?;
Ok((
key_package_bytes,
verifying_key_bytes,
merkle_proof,
merkle_root,
))
})
}
fn dkg_derive_verifying_share(
&self,
secret_key_package_bytes: &[u8],
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
let secret_key_package =
f::keys::KeyPackage::deserialize(secret_key_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
secret_key_package
.verifying_share()
.serialize()
.map_err(|_| ExodusError::SerializationError)
})
}
fn dkg_decompress_key(
&self,
verifying_key_bytes: &[u8],
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
let verifying_key = f::VerifyingKey::deserialize(verifying_key_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
decomporess_key(verifying_key)
})
}
}

View File

@ -1,123 +0,0 @@
use sp_std::{
vec::Vec,
result::Result,
marker::PhantomData,
};
use rand_chacha::rand_core::{CryptoRng, RngCore};
use ghost_traits::exodus::EllipticCurveDiffieHellman;
use hkdf::Hkdf as KeyDerivationFunction;
use sha2::Sha256 as Hashing;
use chacha20poly1305::{
aead::{self, Aead, KeyInit},
Key as EncryptionKey,
Nonce as EncryptionNonce,
ChaCha20Poly1305 as EncryptionCipher,
};
use crate::pallet::Config;
use crate::{AuthIndex, EncryptionData, ExodusError, NetworkCurve, DkgIndex};
impl<T: Config> EllipticCurveDiffieHellman<AuthIndex, EncryptionCipher, EncryptionData<T>, DkgIndex, ExodusError> for NetworkCurve {
fn ecdh_encrypt_package<R: RngCore + CryptoRng>(
&self,
cipher: &EncryptionCipher,
aad: &[u8],
msg: &[u8],
mut rng: R,
) -> Result<EncryptionData<T>, ExodusError> {
let payload = aead::Payload { msg, aad };
let mut nonce_bytes = [0u8; crate::ENCRYPTION_NONCE_MAX_BYTES as usize];
rng.fill_bytes(&mut nonce_bytes);
let nonce = EncryptionNonce::from_slice(&nonce_bytes);
let ciphertext = cipher.encrypt(&nonce, payload)
.map_err(|_| ExodusError::EncryptionFailed)?;
EncryptionData::try_new(ciphertext, nonce.as_slice())
}
fn ecdh_decrypt_package(
&self,
cipher: &EncryptionCipher,
encrypted_data: &EncryptionData<T>,
aad: &[u8],
) -> Result<Vec<u8>, ExodusError> {
let nonce = EncryptionNonce::from_iter(
encrypted_data.nonce.iter().cloned()
);
let payload = aead::Payload { msg: &encrypted_data.ciphertext, aad };
cipher.decrypt(&nonce, payload).map_err(|_| ExodusError::DecryptionFailed)
}
fn ecdh_prepare_additional_info(
&self,
sender_index: AuthIndex,
receiver_index: AuthIndex,
dkg_index: DkgIndex,
_marker: PhantomData<EncryptionData<T>>,
) -> Vec<u8> {
let mut additional_info = match self {
NetworkCurve::Secp256k1 => b"EXODUS-SECP256K1-DKG-V1".to_vec(),
NetworkCurve::Ed25519 => b"EXODUS-ED25519-DKG-V1".to_vec(),
};
additional_info.extend_from_slice(&sender_index.to_be_bytes());
additional_info.extend_from_slice(&receiver_index.to_be_bytes());
additional_info.extend_from_slice(&dkg_index.to_be_bytes());
additional_info
}
fn ecdh_get_cipher_from_keys(
&self,
public: &[u8],
secret: &[u8],
additional_info: &[u8],
_marker: PhantomData<EncryptionData<T>>,
) -> Result<EncryptionCipher, ExodusError> {
with_ciphersuite!(self, |f| {
let secret_package = f::keys::dkg::round1::SecretPackage::deserialize(secret)
.map_err(|_| ExodusError::DeserializationError)?;
let public_package = f::keys::dkg::round1::Package::deserialize(public)
.map_err(|_| ExodusError::DeserializationError)?;
let coefficients = secret_package.coefficients();
let local_sk = coefficients.first()
.ok_or(ExodusError::IncorrectNumberOfCoefficients)?;
let remote_pk = public_package.commitment().coefficients().first()
.ok_or(ExodusError::MissingCommitment)?.value();
let shared_secret_bytes = f::VerifyingKey::new(remote_pk * local_sk)
.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let local_pk: f::VerifyingKey = f::SigningKey::from_scalar(*local_sk)
.map_err(|_| ExodusError::MalformedSigningKey)?
.into();
let local_pk_bytes = local_pk.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let remote_pk_bytes = f::VerifyingKey::new(remote_pk)
.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let mut keys = [local_pk_bytes, remote_pk_bytes];
keys.sort();
let salt = keys.concat();
let hk = KeyDerivationFunction::<Hashing>::new(Some(&salt), &shared_secret_bytes);
let mut encryption_key = EncryptionKey::default();
hk.expand(additional_info, &mut encryption_key)
.map_err(|_| ExodusError::HKDFFailed)?;
let cipher = EncryptionCipher::new_from_slice(&encryption_key)
.expect("could not happen; length is already correct qed");
Ok(cipher)
})
}
}

View File

@ -1,430 +0,0 @@
use ghost_traits::exodus::{
IdentifierConverter, MerkleTreeBuilder,
FlexibleRoundOptimizedSchnorrThresholdSignature,
};
use sp_std::{
vec::Vec, result::Result,
collections::{btree_set::BTreeSet, btree_map::BTreeMap},
};
use rand_chacha::rand_core::{CryptoRng, RngCore};
use ghost_helpers::networks::NetworkCurve;
use crate::{AuthIndex, ExodusHash, ExodusError};
impl FlexibleRoundOptimizedSchnorrThresholdSignature<AuthIndex, ExodusError> for NetworkCurve {
type Packages<'a> = BTreeMap<AuthIndex, &'a [u8]>;
type NoncePackages<'a> = BTreeMap<AuthIndex, (&'a [u8], &'a [u8])>;
fn header_bytes_len(&self) -> usize { 5 }
fn element_bytes_len(&self) -> usize {
match self {
NetworkCurve::Secp256k1 => 33,
NetworkCurve::Ed25519 => 32,
}
}
fn scalar_bytes_len(&self) -> usize {
match self {
NetworkCurve::Secp256k1 => 32,
NetworkCurve::Ed25519 => 32,
}
}
fn verify_signature_share(
&self,
index: AuthIndex,
participants_iter: impl Iterator<Item = AuthIndex>,
signature_share_bytes: &[u8],
binding_factor_bytes: &[u8],
nonce_hiding_bytes: &[u8],
nonce_binding_bytes: &[u8],
group_commitment_bytes: &[u8],
verifying_share_bytes: &[u8],
verifying_key_bytes: &[u8],
message: &[u8],
) -> Result<(), ExodusError> {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
type InnerField = <InnerGroup as frost_core::Group>::Field;
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let identifier_set = participants_iter
.filter_map(|auth_index| {
let non_zero_index = Self::non_zero_index(auth_index).ok()?;
non_zero_index.try_into().ok()
})
.collect::<BTreeSet<f::Identifier>>();
let lambda_i =
frost_core::compute_lagrange_coefficient(&identifier_set, None, identifier)
.map_err(|err| match err {
f::Error::IncorrectNumberOfIdentifiers => ExodusError::IncorrectNumberOfIdentifiers,
f::Error::UnknownIdentifier => ExodusError::UnknownIdentifier,
f::Error::DuplicatedIdentifier => ExodusError::DuplicatedIdentifier,
_ => ExodusError::Unknown,
})?;
let signature_share =
f::round2::SignatureShare::deserialize(signature_share_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let group_commitment_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from(group_commitment_bytes.as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let group_commitment_element =
<InnerGroup as frost_core::Group>::deserialize(&group_commitment_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let binding_factor_serialization =
<InnerField as frost_core::Field>::Serialization::try_from(binding_factor_bytes.as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let binding_factor_scalar =
<InnerField as frost_core::Field>::deserialize(&binding_factor_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let verifying_share =
f::keys::VerifyingShare::deserialize(verifying_share_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let verifying_key = f::VerifyingKey::deserialize(verifying_key_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let challenge = frost_core::challenge(
&group_commitment_element,
&verifying_key,
message,
).map_err(|err| match err {
f::Error::SerializationError => ExodusError::SerializationError,
_ => ExodusError::Unknown,
})?;
let hiding_element_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from(nonce_hiding_bytes.as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let hiding_element =
<InnerGroup as frost_core::Group>::deserialize(&hiding_element_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let binding_element_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from(nonce_binding_bytes.as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let binding_element =
<InnerGroup as frost_core::Group>::deserialize(&binding_element_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let r_i_element = hiding_element + (binding_element * binding_factor_scalar);
let commitment_share =
frost_core::round1::GroupCommitmentShare::from_element(r_i_element);
signature_share.verify(
identifier,
&commitment_share,
&verifying_share,
lambda_i,
&challenge,
).map_err(|err| match err {
f::Error::InvalidSignatureShare { .. } => {
ExodusError::InvalidSignatureShare
},
_ => ExodusError::Unknown,
})
})
}
fn accumulate_signature_scalar(
&self,
signature_scalar_bytes: &[u8],
signature_share_bytes: &[u8],
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
type InnerField = <InnerGroup as frost_core::Group>::Field;
let signature_scalar_serialization =
<InnerField as frost_core::Field>::Serialization::try_from(signature_scalar_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let mut signature_scalar =
<InnerField as frost_core::Field>::deserialize(&signature_scalar_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let signature_share_serialization =
<InnerField as frost_core::Field>::Serialization::try_from(signature_share_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let signature_share =
<InnerField as frost_core::Field>::deserialize(&signature_share_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
signature_scalar += signature_share;
Ok(<InnerField as frost_core::Field>::serialize(&signature_scalar).to_vec())
})
}
fn is_signature_valid(
&self,
verifying_key_bytes: &[u8],
r_element_bytes: &[u8],
z_scalar_bytes: &[u8],
message: &[u8],
) -> Result<(), ExodusError> {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
type InnerField = <InnerGroup as frost_core::Group>::Field;
let verifying_key = f::VerifyingKey::deserialize(verifying_key_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let r_element_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from(r_element_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let r_element =
<InnerGroup as frost_core::Group>::deserialize(&r_element_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let z_scalar_serialization =
<InnerField as frost_core::Field>::Serialization::try_from(z_scalar_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let z_scalar =
<InnerField as frost_core::Field>::deserialize(&z_scalar_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let signature = f::Signature::new(r_element, z_scalar);
verifying_key.verify(message, &signature)
.map_err(|_| ExodusError::InvalidSignature)
})
}
fn generate_nonce_commitment<R: CryptoRng + RngCore>(
&self,
secret_key_package_bytes: &[u8],
mut rng: R
) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), ExodusError> {
with_ciphersuite!(self, |f| {
let signing_share =
f::keys::KeyPackage::deserialize(secret_key_package_bytes)
.map(|key_package| *key_package.signing_share())
.map_err(|_| ExodusError::DeserializationError)?;
let (signing_nonce, signing_commitment) =
f::round1::commit(&signing_share, &mut rng);
let signing_nonce_bytes = signing_nonce.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let hiding_nonce_bytes = signing_commitment.hiding()
.serialize()
.map_err(|_| ExodusError::SerializationError)?;
let binding_nonce_bytes = signing_commitment.binding()
.serialize()
.map_err(|_| ExodusError::SerializationError)?;
Ok((signing_nonce_bytes, hiding_nonce_bytes, binding_nonce_bytes))
})
}
fn generate_group_commitment(
&self,
authority_index: AuthIndex,
nonce_commitments_bytes: &Self::NoncePackages<'_>,
verifying_key_bytes: &[u8],
message: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, Vec<ExodusHash>, ExodusHash), ExodusError> {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
type InnerField = <InnerGroup as frost_core::Group>::Field;
let verifying_key = f::VerifyingKey::deserialize(verifying_key_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let signing_commitments = nonce_commitments_bytes
.iter()
.filter_map(|(&index, &(hiding_bytes, binding_bytes))| {
let non_zero_index = Self::non_zero_index(index).ok()?;
let identifier = non_zero_index.try_into().ok()?;
let hiding_serialization = <InnerGroup as frost_core::Group>::Serialization::try_from(hiding_bytes.as_ref()).ok()?;
let hiding_nonce = f::round1::NonceCommitment::deserialize(&hiding_serialization).ok()?;
let binding_serialization = <InnerGroup as frost_core::Group>::Serialization::try_from((*binding_bytes).as_ref()).ok()?;
let binding_nonce = f::round1::NonceCommitment::deserialize(&binding_serialization).ok()?;
let commitments = f::round1::SigningCommitments::new(hiding_nonce, binding_nonce);
Some((identifier, commitments))
})
.collect::<BTreeMap<_, _>>();
if nonce_commitments_bytes.len() != signing_commitments.len() {
return Err(ExodusError::InvalidParticipantId);
}
let signing_package = f::SigningPackage::new(signing_commitments, message);
let binding_factors_list_wrapped =
frost_core::compute_binding_factor_list(&signing_package, &verifying_key, &[])
.map_err(|err| match err {
f::Error::SerializationError => ExodusError::SerializationError,
_ => ExodusError::Unknown,
})?;
let group_commitment =
frost_core::compute_group_commitment(&signing_package, &binding_factors_list_wrapped)
.map_err(|err| match err {
f::Error::IdentityCommitment => ExodusError::IdentityCommitment,
f::Error::UnknownIdentifier => ExodusError::UnknownIdentifier,
_ => ExodusError::Unknown,
})?;
let binding_factors_list = nonce_commitments_bytes
.iter()
.filter_map(|(&index, _)| {
let non_zero_index = Self::non_zero_index(index).ok()?;
let identifier = non_zero_index.try_into().ok()?;
let binding_factor = binding_factors_list_wrapped
.get(&identifier)
.map(|binding_factor| {
let fixed_bytes =
<InnerField as frost_core::Field>::Serialization::try_from(
binding_factor.serialize().as_slice()
).ok()?;
<InnerField as frost_core::Field>::deserialize(&fixed_bytes).ok()
})
.flatten()?;
Some((identifier, binding_factor))
})
.collect::<BTreeMap<_, _>>();
if nonce_commitments_bytes.len() != binding_factors_list.len() {
return Err(ExodusError::InvalidParticipantId);
}
let binding_factor_tree = Self::build_merkle_tree(
&binding_factors_list,
|binding_factor| {
let serialized = <InnerField as frost_core::Field>::serialize(binding_factor);
Ok(serialized.as_ref().to_vec())
}
)?;
let (self_binding_factor_bytes, binding_factors_proof) =
Self::generate_merkle_proof_from_tree(
&binding_factors_list,
&binding_factor_tree,
authority_index,
|binding_factor| {
let serialized = <InnerField as frost_core::Field>::serialize(binding_factor);
Ok(serialized.as_ref().to_vec())
}
)?;
let group_commitment_bytes =
<InnerGroup as frost_core::Group>::serialize(&group_commitment.to_element())
.map(|serialization| serialization.to_vec())
.map_err(|_| ExodusError::SerializationError)?;
let binding_factors_root = binding_factor_tree
.last()
.cloned()
.ok_or(ExodusError::IncorrectNumberOfPackages)?;
Ok((
group_commitment_bytes,
self_binding_factor_bytes,
binding_factors_proof,
binding_factors_root,
))
})
}
fn init_signing_package(
&self,
nonce_commitments_bytes: &Self::NoncePackages<'_>,
message: &[u8],
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
let signing_commitments = nonce_commitments_bytes
.iter()
.map(|(&index, &(hiding_bytes, binding_bytes))| {
let identifier = Self::non_zero_index(index)?
.try_into()
.map_err(|_| ExodusError::InvalidParticipantId)?;
let hiding_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from(hiding_bytes.as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let hiding_nonce =
f::round1::NonceCommitment::deserialize(&hiding_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let binding_serialization =
<InnerGroup as frost_core::Group>::Serialization::try_from((*binding_bytes).as_ref())
.map_err(|_| ExodusError::DeserializationError)?;
let binding_nonce =
f::round1::NonceCommitment::deserialize(&binding_serialization)
.map_err(|_| ExodusError::DeserializationError)?;
let commitments = f::round1::SigningCommitments::new(hiding_nonce, binding_nonce);
Ok((identifier, commitments))
})
.collect::<Result<BTreeMap<f::Identifier, f::round1::SigningCommitments>, ExodusError>>()?;
f::SigningPackage::new(signing_commitments, message)
.serialize()
.map_err(|_| ExodusError::SerializationError)
})
}
fn partial_sign_message(
&self,
signing_package_bytes: &[u8],
signer_nonces_bytes: &[u8],
secret_share_bytes: &[u8],
) -> Result<Vec<u8>, ExodusError> {
with_ciphersuite!(self, |f| {
let signing_package = f::SigningPackage::deserialize(signing_package_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let signer_nonces = f::round1::SigningNonces::deserialize(signer_nonces_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let secret_share = f::keys::KeyPackage::deserialize(secret_share_bytes)
.map_err(|_| ExodusError::DeserializationError)?;
let signature_shares =
f::round2::sign(&signing_package, &signer_nonces, &secret_share)
.map_err(|err| match err {
f::Error::IncorrectNumberOfCommitments => ExodusError::IncorrectNumberOfCommitments,
f::Error::MissingCommitment => ExodusError::MissingCommitment,
f::Error::IncorrectCommitment => ExodusError::IncorrectCommitment,
f::Error::SerializationError => ExodusError::SerializationError,
f::Error::DuplicatedIdentifier => ExodusError::DuplicatedIdentifier,
f::Error::UnknownIdentifier => ExodusError::UnknownIdentifier,
f::Error::IncorrectNumberOfIdentifiers => ExodusError::IncorrectNumberOfIdentifiers,
_ => ExodusError::Unknown,
})?;
Ok(signature_shares.serialize())
})
}
}

View File

@ -1,127 +0,0 @@
use sp_std::{collections::btree_map::BTreeMap, vec::Vec, result::Result};
use frost_core::{Ciphersuite, Group, Identifier};
use ghost_helpers::SubstrateBlake2Hasher;
use ghost_traits::{
exodus::{MerkleTreeBuilder, IdentifierConverter},
hashing::GhostHasher,
};
use crate::{AuthIndex, ExodusError, NetworkCurve};
impl MerkleTreeBuilder<AuthIndex, ExodusError> for NetworkCurve {
type Hash = <SubstrateBlake2Hasher as GhostHasher>::Hash;
fn build_merkle_tree<C, V, F>(
values: &BTreeMap<Identifier<C>, V>,
serialize_fn: F,
) -> Result<Vec<Self::Hash>, ExodusError>
where
C: frost_core::Ciphersuite,
<<C as Ciphersuite>::Group as Group>::Field: frost_core::Field,
F: Fn(&V) -> Result<Vec<u8>, ExodusError>
{
use ghost_helpers::merkle_tree::generate_tree;
type CField<C> = <<C as Ciphersuite>::Group as Group>::Field;
let max_index = values.keys()
.next_back()
.and_then(|id| {
let id_scalar = id.to_scalar();
let id_bytes = <CField<C> as frost_core::Field>::serialize(&id_scalar);
Self::convert_identifier_to_index(id_bytes.as_ref()).ok()
})
.ok_or(ExodusError::IncorrectNumberOfIdentifiers)?;
generate_tree::<SubstrateBlake2Hasher, _, _, _, _>(
max_index,
values.iter(),
|(identifier, value)| {
let id_scalar = identifier.to_scalar();
let id_bytes = <CField<C> as frost_core::Field>::serialize(&id_scalar);
let index = Self::convert_identifier_to_index(id_bytes.as_ref())?;
let value_bytes = serialize_fn(value).map_err(|_| ExodusError::SerializationError)?;
// NOTE: revisit
// Is it possible to use [u8; CONSTANT] where constant is
// purely dependant on provided generic.
let mut preimage = Vec::<u8>::with_capacity(id_bytes.as_ref().len() + value_bytes.len());
preimage.extend_from_slice(id_bytes.as_ref());
preimage.extend_from_slice(value_bytes.as_ref());
Ok((index as usize, preimage))
})
}
fn generate_merkle_proof_from_tree<C, V, F>(
values: &BTreeMap<Identifier<C>, V>,
merkle_tree: &[Self::Hash],
authority_index: AuthIndex,
serialize_fn: F,
) -> Result<(Vec<u8>, Vec<Self::Hash>), ExodusError>
where
C: frost_core::Ciphersuite,
<<C as Ciphersuite>::Group as Group>::Field: frost_core::Field,
F: Fn(&V) -> Result<Vec<u8>, ExodusError>
{
use ghost_helpers::merkle_tree::generate_proof;
type CField<C> = <<C as Ciphersuite>::Group as Group>::Field;
let non_zero_index = Self::non_zero_index(authority_index)?;
let identifier = Identifier::try_from(non_zero_index)
.map_err(|_| ExodusError::InvalidParticipantId)?;
let value = values.get(&identifier).ok_or(ExodusError::InvalidParticipantId)?;
let value_bytes = serialize_fn(&value).map_err(|_| ExodusError::SerializationError)?;
let max_index = values.keys()
.next_back()
.and_then(|id| {
let id_scalar = id.to_scalar();
let id_bytes = <CField<C> as frost_core::Field>::serialize(&id_scalar);
Self::convert_identifier_to_index(id_bytes.as_ref()).ok()
})
.ok_or(ExodusError::IncorrectNumberOfIdentifiers)?;
let proof = generate_proof::<SubstrateBlake2Hasher, _>(
merkle_tree,
max_index,
authority_index,
);
Ok((value_bytes, proof))
}
fn verify_merkle_proof(
&self,
values: &[u8],
merkle_proof: &[Self::Hash],
merkle_root: Self::Hash,
authority_index: AuthIndex,
) -> Result<(), ExodusError> {
use ghost_helpers::merkle_tree::verify_tree_proof;
let preimage = with_ciphersuite!(self, |f| {
let non_zero_index = Self::non_zero_index(authority_index)?;
let identifier = f::Identifier::try_from(non_zero_index)
.map_err(|_| ExodusError::InvalidParticipantId)?;
let mut preimage = identifier.serialize();
preimage.extend_from_slice(values);
Ok(preimage)
})?;
let is_verified = verify_tree_proof::<SubstrateBlake2Hasher, _>(
&preimage,
merkle_proof,
merkle_root,
authority_index,
);
is_verified.then(|| ()).ok_or(ExodusError::InvalidMerkleProof)
}
}

View File

@ -1,67 +0,0 @@
#[macro_export]
macro_rules! with_ciphersuite {
($curve:expr, |$alias:ident| $body:block) => {
match $curve {
NetworkCurve::Secp256k1 => {
use k256::elliptic_curve::sec1::ToEncodedPoint;
use frost_secp256k1 as $alias;
#[allow(unused_imports)]
use frost_secp256k1::Secp256K1Sha256 as Ciphersuite;
#[allow(dead_code)]
fn decomporess_key(
verifying_key: $alias::VerifyingKey,
) -> Result<Vec<u8>, ExodusError> {
let element = verifying_key.to_element();
if element == k256::ProjectivePoint::IDENTITY {
return Err(ExodusError::InvalidIdentityElement);
}
let mut fixed_serialized = [0; 65];
let serialized_point = element.to_affine().to_encoded_point(false);
let serialized = serialized_point.as_bytes();
fixed_serialized.copy_from_slice(serialized);
Ok(fixed_serialized.to_vec())
}
$body
},
NetworkCurve::Ed25519 => {
use frost_ed25519 as $alias;
#[allow(unused_imports)]
use frost_ed25519::Ed25519Sha512 as Ciphersuite;
#[allow(dead_code)]
fn decomporess_key(
verifying_key: $alias::VerifyingKey,
) -> Result<Vec<u8>, ExodusError> {
verifying_key.serialize().map_err(|err| match err {
f::Error::SerializationError => ExodusError::SerializationError,
_ => ExodusError::Unknown,
})
}
$body
},
}
}
}
#[macro_use]
mod converter;
#[macro_use]
mod merkle;
#[macro_use]
mod dkg;
#[macro_use]
mod ecdh;
#[macro_use]
mod frost;

File diff suppressed because it is too large Load Diff

View File

@ -1,216 +0,0 @@
#![cfg(any(test, feature = "runtime-benchmarks"))]
use frame_system::EnsureRoot;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU64},
};
use crate as ghost_exodus;
use pallet_session::historical as pallet_session_historical;
use sp_runtime::{
BuildStorage, curve::PiecewiseLinear, traits::ConvertInto,
testing::{TestXt, UintAuthorityId},
};
use sp_staking::SessionIndex;
#[cfg(test)]
use ghost_helpers::networks::{NetworkInitiationBuilder, NetworkDataBuilder};
pub type MockedBalance = u64;
pub type MockedNetworkId = u64;
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub enum Runtime {
System: frame_system,
Session: pallet_session,
Historical: pallet_session_historical,
Balances: pallet_balances,
Networks: ghost_networks,
Exodus: ghost_exodus,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type Block = Block;
type AccountData = pallet_balances::AccountData<MockedBalance>;
}
parameter_types! {
pub static FixedAuthorities: Vec<u64> = vec![0, 1, 2, 3];
}
pub struct TestSessionManager;
impl pallet_session::SessionManager<u64> for TestSessionManager {
fn new_session(_new_index: SessionIndex) -> Option<Vec<u64>> {
Some(FixedAuthorities::get())
}
fn end_session(_: SessionIndex) {}
fn start_session(_: SessionIndex) {}
}
impl pallet_session::historical::SessionManager<u64, u64> for TestSessionManager {
fn new_session(_new_index: SessionIndex) -> Option<Vec<(u64, u64)>> {
Some(FixedAuthorities::get().iter().map(|l| (*l, *l)).collect())
}
fn end_session(_: SessionIndex) {}
fn start_session(_: SessionIndex) {}
}
parameter_types! {
pub const Period: u64 = 1;
pub const Offset: u64 = 0;
}
impl pallet_session::Config for Runtime {
type ShouldEndSession = pallet_session::PeriodicSessions<Period, Offset>;
type SessionManager =
pallet_session::historical::NoteHistoricalRoot<Runtime, TestSessionManager>;
type SessionHandler = (Exodus,);
type ValidatorId = u64;
type ValidatorIdOf = ConvertInto;
type Keys = UintAuthorityId;
type RuntimeEvent = RuntimeEvent;
type NextSessionRotation = pallet_session::PeriodicSessions<Period, Offset>;
type WeightInfo = ();
}
impl pallet_session::historical::Config for Runtime {
type FullIdentification = u64;
type FullIdentificationOf = ConvertInto;
}
parameter_types! {
pub const MaxNetworks: u32 = 5;
}
impl ghost_networks::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type NetworkId = MockedNetworkId;
type RegisterOrigin = EnsureRoot<Self::AccountId>;
type UpdateOrigin = EnsureRoot<Self::AccountId>;
type RemoveOrigin = EnsureRoot<Self::AccountId>;
type MaxNetworks = MaxNetworks;
type WeightInfo = ();
}
pallet_staking_reward_curve::build! {
const REWARD_CURVE: PiecewiseLinear<'static> = curve!(
min_inflation: 0_006_000,
max_inflation: 1_000_000,
ideal_stake: 0_690_000,
falloff: 0_050_000,
max_piece_count: 100,
test_precision: 0_005_000,
);
}
parameter_types! {
pub static ExistentialDeposit: u64 = 2;
pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
pub const HistoryDepth: u32 = 10;
pub const EpochDuration: u64 = 80;
}
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl pallet_balances::Config for Runtime {
type ExistentialDeposit = ExistentialDeposit;
type MaxReserves = ConstU32<2>;
type AccountStore = System;
type WeightInfo = ();
}
impl ghost_exodus::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type AuthorityId = UintAuthorityId;
type ValidatorSet = Historical;
type Currency = Balances;
type NetworkDataHandler = Networks;
type BlockNumberProvider = System;
type DisabledValidators = Session;
type UnsignedPriority = ConstU64<{ 1 << 20 }>;
type UnsignedLongevity = ConstU64<1>;
type MaxAuthorities = ConstU32<10>;
type MaxAuthoritiesChunks = ConstU32<1>;
type DkgRoundPeriod = ConstU64<100>;
type RemovalLimit = ConstU32<20>;
type MaxCursorLen = ConstU32<20>;
type WeightInfo = ();
}
pub type Extrinsic<Call> = TestXt<Call, ()>;
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
where
RuntimeCall: From<LocalCall>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = Extrinsic<RuntimeCall>;
}
#[allow(dead_code)]
pub fn new_test_ext_benchmarks() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::<Runtime>::default()
.build_storage()
.unwrap();
sp_io::TestExternalities::new(t)
}
#[cfg(test)]
pub fn new_test_ext() -> (
sp_io::TestExternalities,
std::sync::Arc<parking_lot::RwLock<sp_core::offchain::testing::PoolState>>,
) {
let mut t = frame_system::GenesisConfig::<Runtime>::default()
.build_storage()
.unwrap();
let network_data = NetworkDataBuilder::default()
.with_block_deviation(69)
.build();
let networks = vec![
NetworkInitiationBuilder::<MockedNetworkId, u64>::default()
.with_network_id(31_337)
.with_gatekeeper_amount(0)
.with_network_data(network_data)
.build(),
];
ghost_networks::GenesisConfig::<Runtime> { networks }
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| {
for &id in FixedAuthorities::get().iter() {
System::inc_providers(&id);
assert_eq!(
Session::set_keys(RuntimeOrigin::signed(id), id.into(), vec![],),
Ok(())
);
}
});
let (offchain, _) = sp_core::offchain::testing::TestOffchainExt::with_offchain_db(ext.offchain_db());
let (tx_pool, tx_pool_state) = sp_core::offchain::testing::TestTransactionPoolExt::new();
ext.register_extension(sp_core::offchain::OffchainDbExt::new(offchain.clone()));
ext.register_extension(sp_core::offchain::OffchainWorkerExt::new(offchain));
ext.register_extension(sp_core::offchain::TransactionPoolExt::new(tx_pool));
ext.execute_with(|| System::set_block_number(1));
(ext, tx_pool_state)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
use codec::{Encode, Decode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use frame_support::{BoundedVec, traits::ConstU32};
use crate::{ExodusHash, ELEMENT_MAX_BYTES};
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct ConsensusState<Bitmap> {
pub element_bytes: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
pub participants: Bitmap,
pub merkle_root: ExodusHash,
}
impl<Bitmap> ConsensusState<Bitmap> {
pub fn new(
element_bytes: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
participants: Bitmap,
merkle_root: ExodusHash,
) -> Self {
Self { element_bytes, participants, merkle_root }
}
}

View File

@ -1,295 +0,0 @@
use sp_runtime::{RuntimeDebug, traits::UniqueSaturatedInto};
use codec::{Encode, Decode};
use scale_info::TypeInfo;
use strum::EnumCount;
use frame_support::{BoundedVec, traits::ConstU32};
use ghost_helpers::networks::NetworkType;
use ghost_traits::bounded_bitmap::{
BoundedBitmapGenerator, BoundedBitmapReader, BoundedBitmapWriter,
};
use crate::{ELEMENT_MAX_BYTES, ExodusSession, DkgIndex};
type RotationSessions = BoundedVec<RotationSession, ConstU32<{ NetworkType::COUNT as u32 }>>;
#[derive(Default, Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct ReadyDkgAuthorities<AuthoritiesBitmap> {
authorities_indexes: AuthoritiesBitmap,
dkg_index: DkgIndex,
}
impl<AuthoritiesBitmap> ReadyDkgAuthorities<AuthoritiesBitmap>
where
AuthoritiesBitmap: BoundedBitmapReader,
{
pub fn contains_index<N>(&self, index: N) -> bool
where N: UniqueSaturatedInto<usize>,
{
self.authorities_indexes.contains(index)
}
pub fn get_indexes(&self) -> &AuthoritiesBitmap {
&self.authorities_indexes
}
pub fn get_dkg_index(&self) -> DkgIndex {
self.dkg_index
}
}
impl<AuthoritiesBitmap, Block>
From<&PendingDkgAuthorities<AuthoritiesBitmap, Block>>
for ReadyDkgAuthorities<AuthoritiesBitmap>
where
Block: Copy,
AuthoritiesBitmap: BoundedBitmapWriter + BoundedBitmapReader + BoundedBitmapGenerator + Clone,
for<'a> AuthoritiesBitmap: core::ops::BitAndAssign<&'a AuthoritiesBitmap>,
{
fn from(pending: &PendingDkgAuthorities<AuthoritiesBitmap, Block>) -> Self {
Self {
authorities_indexes: pending.get_indexes().clone(),
dkg_index: pending.get_dkg_index(),
}
}
}
#[derive(Default, Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct RotationSession {
pub exodus_session: ExodusSession,
pub network_type: NetworkType
}
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct PendingDkgAuthorities<AuthoritiesBitmap, Block> {
verifying_key: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
authorities_indexes: AuthoritiesBitmap,
rotation_sessions: RotationSessions,
dkg_state: DkgState<Block>,
}
impl<AuthoritiesBitmap, Block> PendingDkgAuthorities<AuthoritiesBitmap, Block>
where
Block: Copy,
AuthoritiesBitmap: BoundedBitmapWriter + BoundedBitmapReader + BoundedBitmapGenerator + Clone,
for<'a> AuthoritiesBitmap: core::ops::BitAndAssign<&'a AuthoritiesBitmap>,
{
pub fn new_dkg(&mut self, authorities_len: usize) {
self.authorities_indexes = AuthoritiesBitmap::empty(authorities_len);
self.dkg_state.restart();
self.rotation_sessions = Default::default();
}
pub fn set_block(&mut self, block: Block) {
self.dkg_state.set_block(block);
}
pub fn set_verifying_key(
&mut self,
verifying_key: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
) {
self.verifying_key = verifying_key;
}
pub fn nullify_indexes(&mut self) {
self.verifying_key = Default::default();
self.authorities_indexes.nullify();
}
pub fn restart_dkg(&mut self) {
self.verifying_key = Default::default();
self.authorities_indexes.nullify();
self.dkg_state.restart();
self.rotation_sessions = Default::default();
}
pub fn next_phase(&mut self) {
self.dkg_state.next_phase()
}
#[inline(always)]
pub fn intersect_indexes(&mut self, mask: &AuthoritiesBitmap) {
self.authorities_indexes &= mask;
}
pub fn is_dkg_pending(&self) -> bool {
self.dkg_state.is_dkg_pending()
}
pub fn is_dkg_vacant(&self) -> bool {
self.dkg_state.is_dkg_vacant()
}
pub fn get_dkg_index(&self) -> DkgIndex {
self.dkg_state.get_dkg_index()
}
pub fn get_block(&self) -> Block {
self.dkg_state.get_block()
}
pub fn get_phase(&self) -> DkgPhase {
self.dkg_state.get_phase()
}
pub fn get_verifying_key(
&self
) -> BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>> {
self.verifying_key.clone()
}
pub fn is_zero_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round0
}
pub fn is_first_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round1
}
pub fn is_second_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round2
}
pub fn is_third_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round3
}
pub fn is_fourth_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round4
}
pub fn is_fifth_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round5
}
pub fn is_sixth_phase(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Round6
}
pub fn is_dkg_finalized(&self) -> bool {
self.dkg_state.get_phase() == DkgPhase::Finalized
}
pub fn contains_index<N>(&self, index: N) -> bool
where N: UniqueSaturatedInto<usize>,
{
self.authorities_indexes.contains(index)
}
pub fn insert_index<N>(&mut self, i: N) -> Option<N>
where N: Copy + TryInto<usize>
{
self.authorities_indexes.insert(i)
}
pub fn try_push_rotation_session(
&mut self,
exodus_session: ExodusSession,
network_type: NetworkType,
) -> Option<()> {
let rotation_session = RotationSession {
exodus_session,
network_type,
};
self.rotation_sessions.try_push(rotation_session).ok()
}
pub fn count_ones<N>(&self) -> N
where u32: UniqueSaturatedInto<N>,
{
self.authorities_indexes.count_ones()
}
pub fn highest_bit<N>(&self) -> Option<N>
where usize: UniqueSaturatedInto<N>,
{
self.authorities_indexes.highest_bit()
}
pub fn get_indexes(&self) -> &AuthoritiesBitmap {
&self.authorities_indexes
}
pub fn iter_rotation_sessions(&self) -> impl Iterator<Item = &RotationSession> {
self.rotation_sessions.iter()
}
}
#[derive(Default, Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct DkgState<Block> {
block: Block,
index: DkgIndex,
phase: DkgPhase,
}
impl<Block: Copy> DkgState<Block> {
fn is_dkg_pending(&self) -> bool { self.phase.is_dkg_pending() }
fn is_dkg_vacant(&self) -> bool { self.phase.is_dkg_vacant() }
fn get_dkg_index(&self) -> DkgIndex { self.index }
fn get_phase(&self) -> DkgPhase { self.phase }
fn get_block(&self) -> Block { self.block }
fn restart(&mut self) {
self.phase = Default::default();
self.index = self.index.checked_add(1)
.unwrap_or_default();
}
fn next_phase(&mut self) {
self.phase.next_phase()
}
fn set_block(&mut self, block: Block) {
self.block = block;
}
}
#[derive(Default, Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum DkgPhase {
#[default]
Vacant,
Round0,
Round1,
Round2,
Round3,
Round4,
Round5,
Round6,
Round7,
Finalized,
}
impl DkgPhase {
fn is_dkg_pending(&self) -> bool {
match &self {
Self::Finalized | Self::Round7 => false,
_ => true
}
}
fn is_dkg_vacant(&self) -> bool {
match &self {
Self::Vacant => true,
_ => false,
}
}
fn next_phase(&mut self) {
*self = match *self {
Self::Vacant => Self::Round0,
Self::Round0 => Self::Round1,
Self::Round1 => Self::Round2,
Self::Round2 => Self::Round3,
Self::Round3 => Self::Round4,
Self::Round4 => Self::Round5,
Self::Round5 => Self::Round6,
Self::Round6 => Self::Round7,
Self::Round7 => Self::Finalized,
Self::Finalized => Self::Finalized,
};
}
}

View File

@ -1,34 +0,0 @@
use sp_std::vec::Vec;
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use codec::{Encode, Decode};
use frame_support::BoundedVec;
use crate::ExodusError;
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, Ord, PartialOrd, RuntimeDebug, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct EncryptionData<T: crate::pallet::Config> {
pub ciphertext: BoundedVec<u8, crate::CiphertextMaxBytes<T>>,
pub nonce: [u8; crate::ENCRYPTION_NONCE_MAX_BYTES as usize],
}
impl<T: crate::pallet::Config> EncryptionData<T> {
pub fn try_new(
ciphertext_vec: Vec<u8>,
nonce_bytes: &[u8],
) -> Result<Self, ExodusError> {
if nonce_bytes.len() != crate::ENCRYPTION_NONCE_MAX_BYTES as usize {
return Err(ExodusError::EncryptionFailed);
}
let ciphertext =
BoundedVec::<u8, crate::CiphertextMaxBytes<T>>::try_from(ciphertext_vec)
.map_err(|_| ExodusError::EncryptionFailed)?;
let mut nonce = [0u8; crate::ENCRYPTION_NONCE_MAX_BYTES as usize];
nonce.copy_from_slice(nonce_bytes);
Ok(Self { ciphertext, nonce })
}
}

View File

@ -1,236 +0,0 @@
use scale_info::TypeInfo;
use codec::{Encode, Decode};
use sp_runtime::{
Perbill, RuntimeDebug, traits::UniqueSaturatedInto,
helpers_128bit::multiply_by_rational_with_rounding,
};
use crate::{
EvmAddress, EvmBytes32, ExodusSession, RoastSession,
MAX_MESSAGE_SIZE,
};
pub trait EvmAbiEncode {
fn size(&self) -> usize;
fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()>;
}
macro_rules! impl_evm_encodable {
(numeric: $( $ty:ty ),* ) => {
$(
impl EvmAbiEncode for $ty {
#[inline(always)]
fn size(&self) -> usize {
core::mem::size_of::<Self>()
}
#[inline(always)]
fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> {
let val_len = self.size();
let padding = 32 - val_len;
let padding_slice = buffer.get_mut(*cursor..*cursor + padding)?;
padding_slice.fill(0);
*cursor += padding;
let value_slice = buffer.get_mut(*cursor..*cursor + val_len)?;
value_slice.copy_from_slice(&self.to_be_bytes());
*cursor += val_len;
Some(())
}
}
)*
};
(bytes_fixed: $( $ty:ty ),* ) => {
$(
impl EvmAbiEncode for $ty {
#[inline(always)]
fn size(&self) -> usize {
<Self as AsRef<[u8]>>::as_ref(self).len()
}
#[inline(always)]
fn abi_encode(&self, buffer: &mut [u8], cursor: &mut usize) -> Option<()> {
if self.size() > 32 { return None; }
let val_len = self.size();
let padding = 32 - val_len;
let padding_slice = buffer.get_mut(*cursor..*cursor + padding)?;
padding_slice.fill(0);
*cursor += padding;
let value_slice = buffer.get_mut(*cursor..*cursor + val_len)?;
value_slice.copy_from_slice(self.as_ref());
*cursor += val_len;
Some(())
}
}
)*
};
}
impl_evm_encodable!(numeric: u8, u16, u32, u64, u128);
impl_evm_encodable!(bytes_fixed: EvmAddress, EvmBytes32);
macro_rules! abi_encode_evm_message {
($buffer:expr, $sig:expr, [ $( $val:expr ),* $(,)? ]) => {{
let mut cursor = 0;
let mut no_overflow = true;
let hash = sp_io::hashing::keccak_256($sig.as_bytes());
if let Some(selector_slice) = $buffer.get_mut(0..4) {
selector_slice.copy_from_slice(&hash[0..4]);
cursor += 4;
$(
if no_overflow {
if EvmAbiEncode::abi_encode($val, $buffer, &mut cursor).is_none() {
no_overflow = false;
}
}
)*
} else {
no_overflow = false;
}
no_overflow.then(|| &$buffer[0..cursor])
}};
}
#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub struct ExodusRequest<NetworkId, Balance> {
pub r#type: ExodusRequestType<NetworkId, Balance>,
pub next_roast_session: RoastSession,
}
#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub enum ExodusRequestType<NetworkId, Balance> {
EvmRotation {
exodus_session: ExodusSession,
public_key: EvmBytes32,
parity: u8,
},
UtxoRotation,
EvmBridgeOut {
exodus_session: ExodusSession,
network_id: NetworkId,
amount: Balance,
commission: Perbill,
receiver: EvmAddress,
},
EvmGovernance { network_id: NetworkId },
UtxoBridgeOut { network_id: NetworkId },
}
impl<NetworkId, Balance> ExodusRequestType<NetworkId, Balance>
where
NetworkId: UniqueSaturatedInto<u64> + Copy,
Balance: UniqueSaturatedInto<u128> + Copy,
{
pub fn get_message<'a>(
&self,
message_buffer: &'a mut [u8; MAX_MESSAGE_SIZE as usize],
) -> Option<&'a [u8]> {
match &self {
ExodusRequestType::EvmRotation { exodus_session, public_key, parity } => {
abi_encode_evm_message!(
message_buffer,
"rotate(uint256,bytes32,uint8)",
[exodus_session, public_key, parity]
)
},
ExodusRequestType::EvmBridgeOut { exodus_session, network_id, amount, commission, receiver } => {
let mut buffer = [0u8; 32];
buffer[0..20].copy_from_slice(receiver.as_ref());
let chain_id_u64: u64 = (*network_id).unique_saturated_into();
buffer[20..28].copy_from_slice(&chain_id_u64.to_be_bytes());
let scaled_commission = multiply_by_rational_with_rounding(
commission.deconstruct() as u128,
1u128 << 32,
1_000_000_000,
sp_runtime::Rounding::Down
)?;
let commission_uint32 = scaled_commission as u32;
buffer[28..32].copy_from_slice(&commission_uint32.to_be_bytes());
let mut amount_buffer = [0u8; 32];
let amount_u128: u128 = (*amount).unique_saturated_into();
amount_buffer[16..32].copy_from_slice(&amount_u128.to_be_bytes());
let packed_metadata = EvmBytes32::from(buffer);
let amount_u256 = EvmBytes32::from(amount_buffer);
abi_encode_evm_message!(
message_buffer,
"materialize(uint256,uint256,uint256)",
[exodus_session, &amount_u256, &packed_metadata]
)
}
_ => None
}
}
}
impl<NetworkId, Balance> ExodusRequest<NetworkId, Balance>
where
NetworkId: UniqueSaturatedInto<u64> + Copy,
Balance: UniqueSaturatedInto<u128> + Copy,
{
pub fn get_message<'a>(
&self,
message_buffer: &'a mut [u8; MAX_MESSAGE_SIZE as usize],
) -> Option<&'a [u8]> {
self.r#type.get_message(message_buffer)
}
pub fn evm_rotation(
exodus_session: ExodusSession,
public_key: EvmBytes32,
parity: u8,
) -> Self {
let rotation = ExodusRequestType::EvmRotation {
exodus_session,
public_key,
parity,
};
Self::default_request_for(rotation)
}
pub fn evm_bridge_out(
exodus_session: ExodusSession,
network_id: NetworkId,
amount: Balance,
commission: Perbill,
receiver: EvmAddress,
) -> Self {
let evm_bridge_out = ExodusRequestType::EvmBridgeOut {
exodus_session,
network_id,
amount,
commission,
receiver,
};
Self::default_request_for(evm_bridge_out)
}
pub fn is_rotation(&self) -> bool {
match &self.r#type {
ExodusRequestType::EvmRotation { .. } | ExodusRequestType::UtxoRotation { .. } => true,
_ => false,
}
}
fn default_request_for(
request_type: ExodusRequestType<NetworkId, Balance>,
) -> Self {
Self { r#type: request_type, next_roast_session: Default::default() }
}
}

View File

@ -1,611 +0,0 @@
use codec::{Encode, Decode};
use sp_std::{vec::Vec, collections::btree_map::BTreeMap};
use sp_runtime::{
RuntimeDebug, RuntimeAppPublic,
traits::{AtLeast32BitUnsigned, Saturating},
offchain::storage::StorageValueRef,
};
use super::{DkgPackage, ExodusPackage, ExodusOk, PackageContext};
use crate::{AuthIndex, ExodusSession, ExodusHash, ExodusError};
fn create_storage_key(prefix: &[u8], index: &[u8], network: &[u8]) -> Vec<u8> {
let mut key = crate::DB_STORAGE_PREFIX.to_vec();
key.extend(prefix);
key.extend(index);
key.extend(network);
key
}
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct PersistentData<BlockNumber, DataType> {
pub release_block: BlockNumber,
pub data: DataType,
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub enum DkgStorageEnum {
Round0 { package_hash: ExodusHash },
Round1 {
secret_package: Vec<u8>,
public_package: Vec<u8>,
},
Round2 {
secret_package: Vec<u8>,
public_packages: BTreeMap<AuthIndex, Vec<u8>>,
},
Round3 { decrypted: BTreeMap<AuthIndex, Vec<u8>> },
Round4,
Round5 {
secret_share: Vec<u8>,
merkle_proof: Vec<ExodusHash>,
},
Round6,
}
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct ExodusNonce(Vec<u8>);
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct ExodusGroup {
pub self_binding_factor: Vec<u8>,
pub binding_factors_proof: Vec<ExodusHash>,
}
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct ExodusShare;
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
pub struct ExodusExile;
type ExodusNonces<BlockNumber> = BTreeMap<ExodusSession, PersistentData<BlockNumber, ExodusNonce>>;
type ExodusGroups<BlockNumber> = BTreeMap<ExodusSession, PersistentData<BlockNumber, ExodusGroup>>;
type ExodusShares<BlockNumber> = BTreeMap<ExodusSession, PersistentData<BlockNumber, ExodusShare>>;
type ExodusExiles<BlockNumber> = BTreeMap<ExodusSession, PersistentData<BlockNumber, ExodusExile>>;
pub struct ExodusStorageWrapper<ExodusInnerType> {
pub inner: ExodusInnerType,
pub data_changed: bool,
}
impl<ExodusInnerType> ExodusStorageWrapper<ExodusInnerType> {
pub fn new(inner: ExodusInnerType) -> Self {
Self { inner, data_changed: false }
}
}
#[derive(Default)]
pub struct ExodusStorage<BlockNumber, NetworkCurve>
where
BlockNumber: Default,
NetworkCurve: Default,
{
current_block: BlockNumber,
block_longevity: BlockNumber,
pub network_curve: NetworkCurve,
pub authority_index: AuthIndex,
}
impl<B, NC> ExodusStorage<B, NC>
where
B: AtLeast32BitUnsigned + Copy,
B: Encode + Decode + Default,
NC: Encode + Decode + Default + Copy,
{
pub fn with_network_curve(mut self, network_curve: NC) -> Self {
self.network_curve = network_curve;
self
}
pub fn with_current_block(mut self, current_block: B) -> Self {
self.current_block = current_block;
self
}
pub fn with_block_longevity(mut self, block_longevity: B) -> Self {
self.block_longevity = block_longevity;
self
}
pub fn with_authority_index(mut self, authority_index: AuthIndex) -> Self {
self.authority_index = authority_index;
self
}
pub fn read_local_storage(&self) -> ExodusRecords<B> {
let encoded_index = self.authority_index.encode();
let encoded_network = self.network_curve.encode();
let nonces_key = create_storage_key(crate::EXODUS_PREFIX_NONCES, &encoded_index, &encoded_network);
let exodus_nonces = StorageValueRef::persistent(&nonces_key)
.get::<ExodusNonces<B>>()
.ok()
.flatten()
.unwrap_or_default();
let shares_key = create_storage_key(crate::EXODUS_PREFIX_SHARES, &encoded_index, &encoded_network);
let exodus_shares = StorageValueRef::persistent(&shares_key)
.get::<ExodusShares<B>>()
.ok()
.flatten()
.unwrap_or_default();
let groups_key = create_storage_key(crate::EXODUS_PREFIX_BLENDS, &encoded_index, &encoded_network);
let exodus_groups = StorageValueRef::persistent(&groups_key)
.get::<ExodusGroups<B>>()
.ok()
.flatten()
.unwrap_or_default();
let exiles_key = create_storage_key(crate::EXODUS_PREFIX_EXILES, &encoded_index, &encoded_network);
let exodus_exiles = StorageValueRef::persistent(&exiles_key)
.get::<ExodusExiles<B>>()
.ok()
.flatten()
.unwrap_or_default();
ExodusRecords {
exodus_nonces: ExodusStorageWrapper::new(exodus_nonces),
exodus_shares: ExodusStorageWrapper::new(exodus_shares),
exodus_groups: ExodusStorageWrapper::new(exodus_groups),
exodus_exiles: ExodusStorageWrapper::new(exodus_exiles),
}
}
pub fn prepare_signed_call_with_context<T, A, F1, F2, F3>(
&self,
authority_key: &A,
exodus_records: &mut ExodusRecords<B>,
make_data: F1,
submit_call: F2,
insert_data: F3,
) -> Result<(), ExodusError>
where
T: crate::pallet::Config,
A: RuntimeAppPublic,
F1: FnOnce(PackageContext<NC>) -> Result<ExodusPackage<NC, T>, ExodusError>,
F2: FnOnce(ExodusPackage<NC, T>, A::Signature) -> Result<(), ExodusError>,
F3: FnOnce(&mut ExodusRecords<B>, B),
{
let context = PackageContext::default()
.with_authority_index(self.authority_index)
.with_network_curve(self.network_curve);
let package = make_data(context)?;
let signature = authority_key
.sign(&package.encode())
.ok_or(ExodusError::BadSignature(self.authority_index))?;
submit_call(package, signature)?;
let next_release_block = self.current_block.saturating_add(self.block_longevity);
insert_data(exodus_records, next_release_block);
self.try_write_local_storage(exodus_records);
Ok(())
}
pub fn force_clear_local_storage(&self) {
let encoded_index = self.authority_index.encode();
let encoded_network = self.network_curve.encode();
let nonces_key = create_storage_key(crate::EXODUS_PREFIX_NONCES, &encoded_index, &encoded_network);
let shares_key = create_storage_key(crate::EXODUS_PREFIX_SHARES, &encoded_index, &encoded_network);
let groups_key = create_storage_key(crate::EXODUS_PREFIX_BLENDS, &encoded_index, &encoded_network);
let exiles_key = create_storage_key(crate::EXODUS_PREFIX_EXILES, &encoded_index, &encoded_network);
StorageValueRef::persistent(&nonces_key).clear();
StorageValueRef::persistent(&shares_key).clear();
StorageValueRef::persistent(&groups_key).clear();
StorageValueRef::persistent(&exiles_key).clear();
}
pub fn try_write_local_storage(&self, records: &ExodusRecords<B>) {
let encoded_index = self.authority_index.encode();
let encoded_network = self.network_curve.encode();
if records.exodus_nonces.data_changed {
let nonces_key = create_storage_key(crate::EXODUS_PREFIX_NONCES, &encoded_index, &encoded_network);
StorageValueRef::persistent(&nonces_key).set(&records.exodus_nonces.inner);
}
if records.exodus_shares.data_changed {
let shares_key = create_storage_key(crate::EXODUS_PREFIX_SHARES, &encoded_index, &encoded_network);
StorageValueRef::persistent(&shares_key).set(&records.exodus_shares.inner);
}
if records.exodus_groups.data_changed {
let groups_key = create_storage_key(crate::EXODUS_PREFIX_BLENDS, &encoded_index, &encoded_network);
StorageValueRef::persistent(&groups_key).set(&records.exodus_groups.inner);
}
if records.exodus_exiles.data_changed {
let exiles_key = create_storage_key(crate::EXODUS_PREFIX_EXILES, &encoded_index, &encoded_network);
StorageValueRef::persistent(&exiles_key).set(&records.exodus_exiles.inner);
}
}
}
pub struct ExodusRecords<BlockNumber>
where
BlockNumber: AtLeast32BitUnsigned,
{
exodus_nonces: ExodusStorageWrapper<ExodusNonces<BlockNumber>>,
exodus_shares: ExodusStorageWrapper<ExodusShares<BlockNumber>>,
exodus_groups: ExodusStorageWrapper<ExodusGroups<BlockNumber>>,
exodus_exiles: ExodusStorageWrapper<ExodusExiles<BlockNumber>>,
}
impl<BlockNumber> ExodusRecords<BlockNumber>
where
BlockNumber: AtLeast32BitUnsigned + Default,
{
pub fn get_secret_nonce(&self, exodus_session: &ExodusSession) -> Option<&[u8]> {
self.exodus_nonces.inner
.get(exodus_session)
.map(|v| v.data.0.as_slice())
}
pub fn get_group_record(
&self,
exodus_session: &ExodusSession,
) -> Option<ExodusGroup> {
self.exodus_groups.inner
.get(exodus_session)
.cloned()
.map(|storage| storage.data)
}
pub fn is_exodus_session_banned(
&self,
exodus_session: &ExodusSession,
) -> bool {
self.exodus_exiles.inner.contains_key(exodus_session)
}
pub fn is_group_registered(
&self,
exodus_session: &ExodusSession,
) -> bool {
self.exodus_groups.inner.contains_key(exodus_session)
}
pub fn is_waiting_period_for_nonce(
&self,
exodus_session: &ExodusSession,
current_block: &BlockNumber
) -> bool {
self.exodus_nonces.inner
.get(exodus_session)
.map(|data| data.release_block.gt(current_block))
.unwrap_or(false)
}
pub fn is_waiting_period_for_share(
&self,
exodus_session: &ExodusSession,
current_block: &BlockNumber,
) -> bool {
self.exodus_shares.inner
.get(exodus_session)
.map(|data| data.release_block.gt(&current_block))
.unwrap_or(false)
}
pub fn is_waiting_period_for_group(
&self,
exodus_session: &ExodusSession,
current_block: &BlockNumber,
) -> bool {
self.exodus_groups.inner
.get(exodus_session)
.map(|data| data.release_block.gt(&current_block))
.unwrap_or(false)
}
pub fn exclude_exodus_session(
&mut self,
exodus_session: ExodusSession,
) {
let exodus_exile = PersistentData {
data: ExodusExile,
release_block: Default::default(),
};
self.exodus_exiles.inner.insert(exodus_session, exodus_exile);
self.exodus_exiles.data_changed = true;
}
pub fn insert_exodus_nonce(
&mut self,
exodus_session: ExodusSession,
next_release_block: BlockNumber,
nonce_commitment: Vec<u8>,
) {
let exodus_nonce = PersistentData {
data: ExodusNonce(nonce_commitment),
release_block: next_release_block,
};
self.exodus_nonces.inner.insert(exodus_session, exodus_nonce);
self.exodus_nonces.data_changed = true;
}
pub fn insert_exodus_share(
&mut self,
exodus_session: ExodusSession,
next_release_block: BlockNumber,
) {
let exodus_share = PersistentData {
data: ExodusShare,
release_block: next_release_block,
};
self.exodus_shares.inner.insert(exodus_session, exodus_share);
self.exodus_shares.data_changed = true;
}
pub fn insert_exodus_group(
&mut self,
exodus_session: ExodusSession,
next_release_block: BlockNumber,
self_binding_factor: Vec<u8>,
binding_factors_proof: Vec<ExodusHash>,
) {
let exodus_group = PersistentData {
data: ExodusGroup { self_binding_factor, binding_factors_proof },
release_block: next_release_block,
};
self.exodus_groups.inner.insert(exodus_session, exodus_group);
self.exodus_groups.data_changed = true;
}
}
#[derive(Default, Clone, Copy)]
pub struct DkgStorage<BlockNumber, NetworkCurve>
where
BlockNumber: Copy + Clone,
NetworkCurve: Copy + Clone,
{
block_longevity: BlockNumber,
current_block: BlockNumber,
pub network_curve: NetworkCurve,
pub authority_index: AuthIndex,
}
impl<B, NC> DkgStorage<B, NC>
where
B: Saturating + Encode + Decode + Copy + Default + PartialOrd,
NC: Encode + Copy + Default,
{
pub fn with_network_curve(mut self, network_curve: NC) -> Self {
self.network_curve = network_curve;
self
}
pub fn with_current_block(mut self, current_block: B) -> Self {
self.current_block = current_block;
self
}
pub fn with_block_longevity(mut self, block_longevity: B) -> Self {
self.block_longevity = block_longevity;
self
}
pub fn with_authority_index(mut self, authority_index: AuthIndex) -> Self {
self.authority_index = authority_index;
self
}
pub fn get_release_block(&self) -> B {
self.current_block.saturating_add(self.block_longevity)
}
pub fn get_dkg_result(
&self,
dkg_record: DkgRecord<B>,
) -> Result<ExodusOk<NC>, ExodusError> {
match dkg_record.prefix_slice {
crate::DKG_ROUND0_PREFIX => return Ok(ExodusOk::DkgRoundRegistered(0u8, self.authority_index, self.network_curve)),
crate::DKG_ROUND1_PREFIX => return Ok(ExodusOk::DkgRoundRegistered(1u8, self.authority_index, self.network_curve)),
crate::DKG_ROUND2_PREFIX => return Ok(ExodusOk::DkgRoundRegistered(2u8, self.authority_index, self.network_curve)),
crate::DKG_ROUND3_PREFIX => return Ok(ExodusOk::DkgRoundRegistered(3u8, self.authority_index, self.network_curve)),
crate::DKG_ROUND4_PREFIX => return Ok(ExodusOk::DkgRoundRegistered(4u8, self.authority_index, self.network_curve)),
_ => {}
}
if !dkg_record.prefix_slice.starts_with(crate::DKG_SECRET_PREFIX) {
return Err(ExodusError::UnknownPrefix);
}
match &dkg_record.maybe_stored_data {
Some(PersistentData { data: DkgStorageEnum::Round5 { .. }, .. }) => {
Ok(ExodusOk::DkgRoundRegistered(5u8, self.authority_index, self.network_curve))
},
Some(PersistentData { data: DkgStorageEnum::Round6 { .. }, .. }) => {
Ok(ExodusOk::DkgRoundRegistered(6u8, self.authority_index, self.network_curve))
},
_ => Err(ExodusError::UnknownPrefix),
}
}
pub fn read_local_storage<'a>(
&self,
prefix_slice: &'a [u8],
authority_index: AuthIndex,
) -> DkgRecord<'a, B> {
let encoded_index = authority_index.encode();
let encoded_network = self.network_curve.encode();
let key = create_storage_key(prefix_slice, &encoded_index, &encoded_network);
let maybe_stored_data = StorageValueRef::persistent(&key)
.get::<PersistentData<B, DkgStorageEnum>>()
.ok()
.flatten();
DkgRecord { prefix_slice, maybe_stored_data, current_block: self.current_block }
}
pub fn prepare_signed_call_with_context<T, A, F1, F2>(
&self,
authority_key: &A,
dkg_record: &mut DkgRecord<B>,
make_data: F1,
submit_call: F2,
) -> Result<(), ExodusError>
where
T: crate::pallet::Config,
A: RuntimeAppPublic,
F1: FnOnce(PackageContext<NC>) -> Result<DkgPackage<NC, T>, ExodusError>,
F2: FnOnce(DkgPackage<NC, T>, A::Signature) -> Result<(), ExodusError>,
{
let context = PackageContext::default()
.with_authority_index(self.authority_index)
.with_network_curve(self.network_curve);
let package = make_data(context)?;
let signature = authority_key
.sign(&package.encode())
.ok_or(ExodusError::BadSignature(self.authority_index))?;
submit_call(package, signature)?;
self.try_write_local_storage(dkg_record);
Ok(())
}
pub fn try_write_local_storage(
&self,
dkg_record: &DkgRecord<B>,
) {
let encoded_index = self.authority_index.encode();
let encoded_network = self.network_curve.encode();
let key = create_storage_key(
dkg_record.prefix_slice,
&encoded_index,
&encoded_network,
);
match &dkg_record.maybe_stored_data {
Some(to_store) => StorageValueRef::persistent(&key).set(&to_store),
None => StorageValueRef::persistent(&key).clear(),
};
}
pub fn clear_local_storage<'a>(
&self,
prefix: &'a [u8],
) {
let encoded_index = self.authority_index.encode();
let encoded_network = self.network_curve.encode();
let key = create_storage_key(prefix, &encoded_index, &encoded_network);
StorageValueRef::persistent(&key).clear();
}
}
pub struct DkgRecord<'a, BlockNumber> {
maybe_stored_data: Option<PersistentData<BlockNumber, DkgStorageEnum>>,
current_block: BlockNumber,
prefix_slice: &'a [u8],
}
impl<'a, BlockNumber> DkgRecord<'a, BlockNumber>
where
BlockNumber: Default + Copy + PartialOrd,
{
pub fn is_waiting_period(&self) -> bool {
self.maybe_stored_data
.as_ref()
.map(|data| data.release_block.gt(&self.current_block))
.unwrap_or(false)
}
pub fn storage_exists(&self) -> bool {
self.maybe_stored_data.is_some()
}
pub fn into_inner(&self) -> Option<&DkgStorageEnum> {
self.maybe_stored_data.as_ref().map(|stored| &stored.data)
}
pub fn into_inner_mut(&mut self) -> Option<&mut DkgStorageEnum> {
self.maybe_stored_data.as_mut().map(|stored| &mut stored.data)
}
pub fn try_update_max_signers<F>(
&mut self,
get_new_secret: F
) -> Result<&Vec<u8>, ExodusError>
where
F: FnOnce(&[u8]) -> Result<Vec<u8>, ExodusError>,
{
enum RoundType<'a> {
R1 { public_package: &'a Vec<u8> },
R2 { public_packages: &'a BTreeMap<AuthIndex, Vec<u8>> },
}
let (old_secret, round_type) = self.maybe_stored_data.as_ref()
.map(|persistent| match &persistent.data {
DkgStorageEnum::Round1 { secret_package, public_package } => {
Ok((secret_package, RoundType::R1 { public_package }))
},
DkgStorageEnum::Round2 { secret_package, public_packages } => {
Ok((secret_package, RoundType::R2 { public_packages }))
},
_ => Err(ExodusError::NothingToWrite),
})
.ok_or(ExodusError::NothingToWrite)??;
let new_secret = get_new_secret(old_secret)?;
let new_data = match round_type {
RoundType::R1 { public_package } => DkgStorageEnum::Round1 {
secret_package: new_secret,
public_package: public_package.clone(),
},
RoundType::R2 { public_packages } => DkgStorageEnum::Round2 {
secret_package: new_secret,
public_packages: public_packages.clone(),
},
};
self.update_with_block(None, new_data)?;
self.maybe_stored_data.as_ref()
.map(|persistent| match &persistent.data {
DkgStorageEnum::Round1 { secret_package, .. } |
DkgStorageEnum::Round2 { secret_package, .. } => Ok(secret_package),
_ => Err(ExodusError::NothingToWrite),
})
.ok_or(ExodusError::NothingToWrite)?
}
pub fn update_with_block(
&mut self,
maybe_release_block: Option<BlockNumber>,
new_data: DkgStorageEnum,
) -> Result<(), ExodusError> {
use core::mem::discriminant as d;
let release_block = match &self.maybe_stored_data {
Some(existing) => {
if d(&existing.data) != d(&new_data) {
return Err(ExodusError::NothingToWrite);
}
maybe_release_block.unwrap_or(existing.release_block)
}
None => maybe_release_block.unwrap_or_default(),
};
self.maybe_stored_data = Some(PersistentData { release_block, data: new_data });
Ok(())
}
}

View File

@ -1,23 +0,0 @@
mod exodus_request;
mod local_storage;
mod encryption;
mod consensus;
mod signature;
mod packages;
mod success;
mod removal;
mod nonce;
mod roast;
mod dkg;
pub use exodus_request::*;
pub use local_storage::*;
pub use encryption::*;
pub use consensus::*;
pub use signature::*;
pub use packages::*;
pub use success::*;
pub use removal::*;
pub use nonce::*;
pub use roast::*;
pub use dkg::*;

View File

@ -1,20 +0,0 @@
use frame_support::{BoundedVec, traits::ConstU32};
use codec::{Encode, Decode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use crate::ELEMENT_MAX_BYTES;
type NonceType = BoundedVec<u8, ConstU32<{ ELEMENT_MAX_BYTES }>>;
#[derive(Default, Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub struct ExodusSeparatedNonce {
pub hiding: NonceType,
pub binding: NonceType,
}
impl ExodusSeparatedNonce {
pub fn new(hiding: NonceType, binding: NonceType) -> Self {
Self { hiding, binding }
}
}

View File

@ -1,466 +0,0 @@
use sp_std::vec::Vec;
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use codec::{Encode, Decode};
use frame_support::{BoundedVec, traits::ConstU32};
use crate::{AuthIndex, ExodusSession, ExodusHash, ExodusError};
pub trait PackageMetadata<NetworkCurve>
where
NetworkCurve: Copy,
{
fn context(&self) -> &PackageContext<NetworkCurve>;
fn get_authority_index(&self) -> AuthIndex {
self.context().get_authority_index()
}
fn get_network_curve(&self) -> NetworkCurve {
self.context().get_network_curve()
}
}
#[derive(Encode, Decode, Clone, RuntimeDebug, TypeInfo, PartialEq, Eq)]
#[scale_info(skip_type_params(T))]
pub enum ExodusPackage<NetworkCurve, T>
where
T: crate::pallet::Config,
{
NonceCommitment(ExodusNonceCommitment<NetworkCurve>),
GroupCommitment(ExodusGroupCommitment<NetworkCurve>),
SignatureShare(ExodusSignatureShare<NetworkCurve, T>),
}
impl<NetworkCurve, T> ExodusPackage<NetworkCurve, T>
where
T: crate::pallet::Config,
{
pub fn bytes_len(&self) -> u32 {
let length = match self {
Self::GroupCommitment(package) => package.group_commitment.len(),
Self::SignatureShare(package) => {
(package.binding_factors_proof.len() * ExodusHash::len_bytes())
+ package.self_binding_factor.len()
+ package.signature_share.len()
},
Self::NonceCommitment(package) => {
package.hiding_commitment.len()
+ package.binding_commitment.len()
},
};
length as u32
}
pub fn get_exodus_session(&self) -> ExodusSession {
match &self {
Self::SignatureShare(package) => package.session,
Self::NonceCommitment(package) => package.session,
Self::GroupCommitment(package) => package.session,
}
}
}
#[derive(Encode, Decode, Clone, RuntimeDebug, TypeInfo, PartialEq, Eq)]
#[scale_info(skip_type_params(T))]
pub enum DkgPackage<NetworkCurve, T>
where
T: crate::pallet::Config,
{
Round0(Round0Package<NetworkCurve>),
Round1(Round1Package<NetworkCurve, T>),
Round2(Round2Package<NetworkCurve, T>),
Round3(Round3Package<NetworkCurve, T>),
Round4(Round4Package<NetworkCurve, T>),
Round5(Round5Package<NetworkCurve>),
Round6(Round6Package<NetworkCurve, T>),
}
impl<NetworkCurve, T> DkgPackage<NetworkCurve, T>
where
T: crate::pallet::Config,
{
pub fn bytes_len(&self) -> u32 {
let length = match self {
Self::Round0(_) => ExodusHash::len_bytes(),
Self::Round1(round1) => round1.package.len(),
Self::Round2(round2) => round2.bundle.bitmask.len(),
Self::Round3(round3) => round3.indices.len(),
Self::Round4(round4) => round4.bundle.bitmask.len(),
Self::Round5(round5) => {
round5.verifying_key.len()
.saturating_add(ExodusHash::len_bytes())
},
Self::Round6(round6) => round6.merkle_proof.len(),
};
length as u32
}
}
#[derive(Default, RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
pub struct PackageContext<NetworkCurve> {
authority_index: AuthIndex,
network_curve: NetworkCurve
}
impl<NetworkCurve> PackageContext<NetworkCurve>
where
NetworkCurve: Copy,
{
pub fn get_authority_index(&self) -> AuthIndex {
self.authority_index
}
pub fn get_network_curve(&self) -> NetworkCurve {
self.network_curve
}
pub fn with_network_curve(mut self, network_curve: NetworkCurve) -> Self {
self.network_curve = network_curve;
self
}
pub fn with_authority_index(mut self, authority_index: AuthIndex) -> Self {
self.authority_index = authority_index;
self
}
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
pub struct BundledPackages {
pub blob: Vec<u8>,
pub bitmask: Vec<u8>,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct EncryptionBundledPackages<T>
where
T: crate::pallet::Config,
{
pub blob: BoundedVec<u8, crate::EncryptedRound2BlobMaxBytes<T>>,
pub bitmask: BoundedVec<u8, crate::MaxAuthoritiesBitmaskSize<T>>
}
impl<T: crate::pallet::Config> EncryptionBundledPackages<T> {
pub fn len(&self) -> usize {
let blob_len = self.blob.len();
let bitmask_len = self.bitmask.len();
bitmask_len.saturating_add(blob_len)
}
}
impl<T: crate::pallet::Config> TryFrom<BundledPackages> for EncryptionBundledPackages<T> {
type Error = ExodusError;
fn try_from(source: BundledPackages) -> Result<Self, Self::Error> {
let blob =
BoundedVec::<u8, crate::EncryptedRound2BlobMaxBytes<T>>::try_from(source.blob)
.map_err(|_| ExodusError::DeserializationError)?;
let bitmask = BoundedVec::<u8, crate::MaxAuthoritiesBitmaskSize<T>>::try_from(source.bitmask)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Self { blob, bitmask })
}
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct JustificationsBundledPackages<T>
where
T: crate::pallet::Config,
{
pub blob: BoundedVec<u8, crate::Round2BlobMaxBytes<T>>,
pub bitmask: BoundedVec<u8, crate::MaxAuthoritiesBitmaskSize<T>>
}
impl<T: crate::pallet::Config> JustificationsBundledPackages<T> {
pub fn len(&self) -> usize {
let blob_len = self.blob.len();
let bitmask_len = self.bitmask.len();
bitmask_len.saturating_add(blob_len)
}
}
impl<T: crate::pallet::Config> TryFrom<BundledPackages> for JustificationsBundledPackages<T> {
type Error = ExodusError;
fn try_from(source: BundledPackages) -> Result<Self, Self::Error> {
let blob =
BoundedVec::<u8, crate::Round2BlobMaxBytes<T>>::try_from(source.blob)
.map_err(|_| ExodusError::DeserializationError)?;
let bitmask = BoundedVec::<u8, crate::MaxAuthoritiesBitmaskSize<T>>::try_from(source.bitmask)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Self { blob, bitmask })
}
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ExodusNonceCommitment<NetworkCurve> {
context: PackageContext<NetworkCurve>,
pub hiding_commitment: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
pub binding_commitment: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
pub session: ExodusSession,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
pub struct ExodusGroupCommitment<NetworkCurve> {
context: PackageContext<NetworkCurve>,
pub group_commitment: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
pub binding_factors_root: ExodusHash,
pub session: ExodusSession,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ExodusSignatureShare<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub binding_factors_proof: BoundedVec<ExodusHash, crate::BindingFactorsProof<T>>,
pub signature_share: BoundedVec<u8, ConstU32<{ crate::SCALAR_MAX_BYTES }>>,
pub self_binding_factor: BoundedVec<u8, ConstU32<{ crate::SCALAR_MAX_BYTES }>>,
pub session: ExodusSession,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
pub struct Round0Package<NetworkCurve> {
context: PackageContext<NetworkCurve>,
pub package_hash: ExodusHash,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct Round1Package<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub package: BoundedVec<u8, crate::Round1MaxBytes<T>>,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct Round2Package<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub bundle: EncryptionBundledPackages<T>,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct Round3Package<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub indices: BoundedVec<u8, crate::AccusedIndicesMaxBytes<T>>,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct Round4Package<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub bundle: JustificationsBundledPackages<T>,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
pub struct Round5Package<NetworkCurve> {
context: PackageContext<NetworkCurve>,
pub verifying_key: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
pub merkle_root: ExodusHash,
}
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct Round6Package<NetworkCurve, T>
where
T: crate::pallet::Config,
{
context: PackageContext<NetworkCurve>,
pub verifying_share: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
pub merkle_proof: BoundedVec<ExodusHash, crate::MerkleProofMaxSize<T>>,
}
impl<NetworkCurve> PackageContext<NetworkCurve> {
pub fn with_exodus_nonce_commitment(
self,
session: ExodusSession,
hiding_commitment_vec: Vec<u8>,
binding_commitment_vec: Vec<u8>,
) -> Result<ExodusNonceCommitment<NetworkCurve>, ExodusError> {
let hiding_commitment =
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(hiding_commitment_vec)
.map_err(|_| ExodusError::DeserializationError)?;
let binding_commitment =
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(binding_commitment_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(ExodusNonceCommitment { context: self, hiding_commitment, binding_commitment, session })
}
pub fn with_exodus_group_commitment(
self,
session: ExodusSession,
commitment_vec: Vec<u8>,
binding_factors_root: ExodusHash,
) -> Result<ExodusGroupCommitment<NetworkCurve>, ExodusError> {
let group_commitment =
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(commitment_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(ExodusGroupCommitment {
context: self,
group_commitment,
binding_factors_root,
session,
})
}
pub fn with_exodus_signature_share<T: crate::pallet::Config>(
self,
session: ExodusSession,
signature_share_vec: Vec<u8>,
binding_factors_proof_vec: Vec<ExodusHash>,
self_binding_factor_vec: Vec<u8>,
) -> Result<ExodusSignatureShare<NetworkCurve, T>, ExodusError> {
let signature_share =
BoundedVec::<u8, ConstU32<{ crate::SCALAR_MAX_BYTES }>>::try_from(signature_share_vec)
.map_err(|_| ExodusError::DeserializationError)?;
let binding_factors_proof =
BoundedVec::<ExodusHash, crate::BindingFactorsProof<T>>::try_from(binding_factors_proof_vec)
.map_err(|_| ExodusError::DeserializationError)?;
let self_binding_factor =
BoundedVec::<u8, ConstU32<{ crate::SCALAR_MAX_BYTES }>>::try_from(self_binding_factor_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(ExodusSignatureShare {
context: self,
binding_factors_proof,
self_binding_factor,
signature_share,
session,
})
}
pub fn with_dkg_round0(
self,
package_hash: ExodusHash,
) -> Round0Package<NetworkCurve> {
Round0Package { context: self, package_hash }
}
pub fn with_dkg_round1<T: crate::pallet::Config>(
self,
package_vec: Vec<u8>,
) -> Result<Round1Package<NetworkCurve, T>, ExodusError> {
let package =
BoundedVec::<u8, crate::Round1MaxBytes<T>>::try_from(package_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Round1Package { context: self, package })
}
pub fn with_dkg_round2<T: crate::pallet::Config>(
self,
bundle_raw: BundledPackages,
) -> Result<Round2Package<NetworkCurve, T>, ExodusError> {
let bundle: EncryptionBundledPackages<T> = bundle_raw.try_into()?;
Ok(Round2Package { context: self, bundle })
}
pub fn with_dkg_round3<T: crate::pallet::Config>(
self,
indices_vec: Vec<u8>
) -> Result<Round3Package<NetworkCurve, T>, ExodusError> {
let indices =
BoundedVec::<u8, crate::AccusedIndicesMaxBytes<T>>::try_from(indices_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Round3Package { context: self, indices })
}
pub fn with_dkg_round4<T: crate::pallet::Config>(
self,
bundle_raw: BundledPackages,
) -> Result<Round4Package<NetworkCurve, T>, ExodusError> {
let bundle: JustificationsBundledPackages<T> = bundle_raw.try_into()?;
Ok(Round4Package { context: self, bundle })
}
pub fn with_dkg_round5(
self,
verifying_key_vec: Vec<u8>,
merkle_root: ExodusHash,
) -> Result<Round5Package<NetworkCurve>, ExodusError> {
let verifying_key =
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(verifying_key_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Round5Package { context: self, verifying_key, merkle_root })
}
pub fn with_dkg_round6<T: crate::pallet::Config>(
self,
verifying_share_vec: Vec<u8>,
merkle_proof_vec: Vec<ExodusHash>,
) -> Result<Round6Package<NetworkCurve, T>, ExodusError> {
let verifying_share =
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(verifying_share_vec)
.map_err(|_| ExodusError::DeserializationError)?;
let merkle_proof =
BoundedVec::<ExodusHash, crate::MerkleProofMaxSize<T>>::try_from(merkle_proof_vec)
.map_err(|_| ExodusError::DeserializationError)?;
Ok(Round6Package { context: self, verifying_share, merkle_proof })
}
}
impl<NC, T> PackageMetadata<NC> for ExodusPackage<NC, T>
where
NC: Copy,
T: crate::pallet::Config,
{
fn context(&self) -> &PackageContext<NC> {
match self {
Self::NonceCommitment(package) => &package.context,
Self::GroupCommitment(package) => &package.context,
Self::SignatureShare(package) => &package.context,
}
}
}
impl<NC, T> PackageMetadata<NC> for DkgPackage<NC, T>
where
NC: Copy,
T: crate::pallet::Config,
{
fn context(&self) -> &PackageContext<NC> {
match self {
Self::Round0(package) => &package.context,
Self::Round1(package) => &package.context,
Self::Round2(package) => &package.context,
Self::Round3(package) => &package.context,
Self::Round4(package) => &package.context,
Self::Round5(package) => &package.context,
Self::Round6(package) => &package.context,
}
}
}

View File

@ -1,24 +0,0 @@
use frame_support::weights::Weight;
pub struct DkgRemovalResult {
weight: Weight,
cleared: bool,
}
impl DkgRemovalResult {
pub fn new<F>(try_clean_storage: F) -> Self
where
F: FnOnce() -> (Weight, bool)
{
let (weight, cleared) = try_clean_storage();
Self { weight, cleared }
}
pub fn fully_cleared(&self) -> bool {
self.cleared
}
pub fn used_weight(&self) -> Weight {
self.weight
}
}

View File

@ -1,22 +0,0 @@
use codec::{Encode, Decode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
#[derive(Default, Encode, Decode, Clone, Copy, PartialEq, TypeInfo, RuntimeDebug)]
pub enum RoastStatus {
#[default]
NonceCommitments,
GroupCommitments,
PartialSignatures,
}
#[derive(Default, Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub struct RoastSessionState<Bitmap>
where
Bitmap: Default,
{
pub status: RoastStatus,
pub nonce_committers: Bitmap,
pub group_committers: Bitmap,
pub partial_signers: Bitmap,
}

View File

@ -1,46 +0,0 @@
use scale_info::TypeInfo;
use codec::{Encode, Decode};
use frame_support::{BoundedVec, traits::ConstU32};
use sp_runtime::{RuntimeDebug, traits::UniqueSaturatedInto};
type ExodusNonceT = BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>;
type ExodusSignatureT = BoundedVec<u8, ConstU32<{ crate::SCALAR_MAX_BYTES }>>;
type Type<NetworkId, Balance> = super::ExodusRequestType<NetworkId, Balance>;
#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, RuntimeDebug)]
pub struct ExodusSignedMessage<NetworkId: Default, Balance: Default> {
pub nonce: ExodusNonceT,
pub signature: ExodusSignatureT,
pub r#type: Type<NetworkId, Balance>,
}
impl<NetworkId, Balance> ExodusSignedMessage<NetworkId, Balance>
where
NetworkId: UniqueSaturatedInto<u64> + Copy + Default,
Balance: UniqueSaturatedInto<u128> + Copy + Default,
{
pub fn new<E, S>(
nonce_iter: E,
signature_iter: S,
r#type: Type<NetworkId, Balance>
) -> Option<Self>
where
E: Iterator<Item = u8> + ExactSizeIterator,
S: Iterator<Item = u8> + ExactSizeIterator,
{
let mut nonce = ExodusNonceT::new();
nonce.try_extend(nonce_iter).ok()?;
let mut signature = ExodusSignatureT::new();
signature.try_extend(signature_iter).ok()?;
Some(Self { nonce, signature, r#type })
}
pub fn get_message<'a>(
&self,
message_buffer: &'a mut [u8; crate::MAX_MESSAGE_SIZE as usize],
) -> Option<&'a [u8]> {
self.r#type.get_message(message_buffer)
}
}

View File

@ -1,44 +0,0 @@
use crate::{AuthIndex, ExodusSession, DkgIndex, RoastSession};
pub enum ExodusOk<NetworkCurve> {
ExodusEmptyRequests(AuthIndex, NetworkCurve),
ExodusWaitingState(AuthIndex, NetworkCurve),
ExodusNonceCommitted(ExodusSession, RoastSession, AuthIndex, NetworkCurve),
ExodusGroupCommitted(ExodusSession, RoastSession, AuthIndex, NetworkCurve),
ExodusSignedPartially(ExodusSession, RoastSession, AuthIndex, NetworkCurve),
ExodusVerifyingShareMissed(AuthIndex, DkgIndex, NetworkCurve),
DkgNotPartOfRound(u8, AuthIndex, NetworkCurve),
DkgRoundRegistered(u8, AuthIndex, NetworkCurve),
DkgPreviousPurged(AuthIndex, NetworkCurve),
DkgRoundsCleared(AuthIndex, NetworkCurve),
DkgRoundAlreadyPassed(u8, AuthIndex, NetworkCurve),
DkgRoundWaitingPeriod(u8, AuthIndex, NetworkCurve),
DkgRoundNothingStored(u8, AuthIndex, NetworkCurve),
DkgRoundMaxSignersNarrowed(u8, AuthIndex, AuthIndex, AuthIndex, NetworkCurve),
}
impl<NetworkCurve> core::fmt::Display for ExodusOk<NetworkCurve>
where
NetworkCurve: core::fmt::Display
{
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
ExodusOk::ExodusEmptyRequests(auth_index, curve) => write!(fmt, "No EXODUS requests found by authority #{auth_index} for curve {curve}."),
ExodusOk::ExodusWaitingState(auth_index, curve) => write!(fmt, "Waiting for EXODUS requests to be processed by authority #{auth_index} for curve {curve}."),
ExodusOk::ExodusNonceCommitted(exodus, roast, auth_index, curve) => write!(fmt, "Nonce committed for EXODUS {exodus:?} on ROAST session {roast:?} by authority #{auth_index} for curve {curve}."),
ExodusOk::ExodusGroupCommitted(exodus, roast, auth_index, curve) => write!(fmt, "Aggregated nonce and binding factor proof committed for EXODUS {exodus:?} on ROAST session {roast:?} by authority #{auth_index} for curve {curve}."),
ExodusOk::ExodusSignedPartially(exodus, roast, auth_index, curve) => write!(fmt, "Partial signature created for EXODUS {exodus:?} on ROAST session {roast:?} by authority #{auth_index} for curve {curve}."),
ExodusOk::ExodusVerifyingShareMissed(auth_index, dkg_index, curve) => write!(fmt, "EXODUS Round no verifying share for authority #{auth_index} based on DKG session #{dkg_index} for curve {curve}."),
ExodusOk::DkgNotPartOfRound(round, auth_index, curve) => write!(fmt, "DKG authority #{auth_index} for network curve {curve} is not part of Round{round}."),
ExodusOk::DkgRoundRegistered(round, auth_index, curve) => write!(fmt, "DKG Round{round} registered by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgPreviousPurged(auth_index, curve) => write!(fmt, "Storage prepared for new DKG session by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgRoundsCleared(auth_index, curve) => write!(fmt, "DKG Rounds cleared from storage by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgRoundAlreadyPassed(round, auth_index, curve) => write!(fmt, "DKG Round{round} already done by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgRoundWaitingPeriod(round, auth_index, curve) => write!(fmt, "DKG Round{round} waiting to be applied by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgRoundNothingStored(round, auth_index, curve) => write!(fmt, "DKG nothing stored from Round{round} by authority #{auth_index} for network curve {curve}."),
ExodusOk::DkgRoundMaxSignersNarrowed(round, auth_index, prev_max, curr_max, curve) => write!(fmt, "DKG Round{round} narrowed max_signers from {prev_max} to {curr_max} by {auth_index} for network curve {curve}."),
}
}
}

View File

@ -1,52 +0,0 @@
use frame_support::weights::Weight;
pub trait WeightInfo {
fn register_round0_package() -> Weight;
fn register_round1_package(package_len: u32) -> Weight;
fn register_encrypted_round2_packages(packages: u32,) -> Weight;
fn register_round3_complaints(indices: u32) -> Weight;
fn register_round4_justifications(packages: u32) -> Weight;
fn register_round5_verifying_package() -> Weight;
fn register_round6_public_share_package(packages: u32) -> Weight;
fn register_nonce_commitment() -> Weight;
fn register_group_commitment() -> Weight;
fn register_signature_share() -> Weight;
fn register_evm_bridge_out_exodus() -> Weight;
}
impl WeightInfo for () {
fn register_round0_package() -> Weight {
Default::default()
}
fn register_round1_package(_package_len: u32) -> Weight {
Default::default()
}
fn register_encrypted_round2_packages(_packages: u32,) -> Weight {
Default::default()
}
fn register_round3_complaints(_indices: u32) -> Weight {
Default::default()
}
fn register_round4_justifications(_packages: u32) -> Weight {
Default::default()
}
fn register_round5_verifying_package() -> Weight {
Default::default()
}
fn register_round6_public_share_package(_packages: u32) -> Weight {
Default::default()
}
fn register_nonce_commitment() -> Weight {
Default::default()
}
fn register_group_commitment() -> Weight {
Default::default()
}
fn register_signature_share() -> Weight {
Default::default()
}
fn register_evm_bridge_out_exodus() -> Weight {
Default::default()
}
}

View File

@ -1,135 +0,0 @@
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_support::dispatch::RawOrigin;
use ghost_helpers::merkle_tree::{generate_tree, generate_proof};
#[benchmarks(where T: Config)]
mod benchmarks {
use super::*;
#[benchmark]
fn claim() {
let network_id = Default::default();
let current_account: T::AccountId = frame_benchmarking::whitelisted_caller();
let max_proof_depth = <<T as Config>::MaxProofDepth as Get<u32>>::get();
let minimum_donation_u128 = <<T as Config>::MinimumDonation as Get<u128>>::get();
let minimum_donation: BalanceOf<T> = minimum_donation_u128.unique_saturated_into();
let dummy_balance_u128 = 420u128;
let dummy_balance: BalanceOf<T> = dummy_balance_u128.unique_saturated_into();
let secret_key = libsecp256k1::SecretKey::parse(&[69u8; 32]).unwrap();
let public_key = libsecp256k1::PublicKey::from_secret_key(&secret_key);
let pubkey_raw = &public_key.serialize()[1..65];
let pubkey_hash = SubstrateKeccakHasher::hash(pubkey_raw);
let mut evm_addr_bytes = [0u8; 20];
evm_addr_bytes.copy_from_slice(&pubkey_hash.as_ref()[12..32]);
let dummy_evm_address = EvmAddress::from(evm_addr_bytes);
let total_entries = 1u64 << max_proof_depth;
let max_index = total_entries - 1;
let raw_values = (0..total_entries)
.map(|i| i as TokenId)
.collect::<Vec<TokenId>>();
let merkle_tree = generate_tree::<SubstrateKeccakHasher, _, _, _, _>(
max_index,
raw_values,
|index: TokenId| -> Result<(usize, Vec<u8>), ()> {
let claim_package = ClaimPackage {
balance: dummy_balance,
merkle_proof: Default::default(),
token_id: index,
};
let claim_preimage = claim_package.get_preimage(&dummy_evm_address);
Ok((index as usize, claim_preimage.to_vec()))
}
).unwrap();
let merkle_root = *merkle_tree.last().unwrap();
let member_share = MemberShare {
initiated_network: None,
activated_shares: Default::default(),
locked_balance: minimum_donation,
};
MemberShares::<T>::insert(&current_account, member_share);
let network_state = NetworkShare::new(
dummy_balance,
merkle_root,
max_proof_depth,
);
NetworkShares::<T>::insert(&network_id, network_state);
let global_state = ShareState::new(dummy_balance);
GlobalShares::<T>::put(global_state);
let merkle_proof = generate_proof::<SubstrateKeccakHasher, _>(
&merkle_tree,
total_entries - 1,
0,
);
let claim_package = ClaimPackage {
merkle_proof: BoundedVec::try_from(merkle_proof).unwrap(),
balance: dummy_balance,
token_id: 0
};
let preimage = claim_package.get_preimage(&dummy_evm_address);
let preimage_hash = SubstrateKeccakHasher::hash(&preimage);
let message_string_bytes = Pallet::<T>::ethereum_signable_message(
&current_account,
&network_id,
&preimage_hash
);
let message_hash = SubstrateKeccakHasher::hash(&message_string_bytes);
let mut message_bytes = [0u8; 32];
message_bytes.copy_from_slice(message_hash.as_ref());
let msg_to_sign = libsecp256k1::Message::parse(&message_bytes);
let (sig, recovery_id) = libsecp256k1::sign(&msg_to_sign, &secret_key);
let mut signature_raw = [0u8; 65];
signature_raw[0..32].copy_from_slice(&sig.r.b32());
signature_raw[32..64].copy_from_slice(&sig.s.b32());
signature_raw[64] = recovery_id.serialize();
let evm_signature = EvmSignature::from(signature_raw);
#[extrinsic_call]
claim(
RawOrigin::Signed(current_account.clone()),
network_id,
claim_package,
dummy_evm_address,
evm_signature,
);
assert_eq!(dummy_balance, GlobalShares::<T>::get().claimed_shares);
assert_eq!(
dummy_balance,
MemberShares::<T>::get(&current_account).activated_shares
);
assert_eq!(
dummy_balance,
NetworkShares::<T>::get(&network_id).claimed_shares(),
);
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(),
crate::mock::Test,
);
}

View File

@ -1,460 +0,0 @@
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
ensure, traits::{Currency, Get},
pallet_prelude::*,
};
use frame_system::pallet_prelude::*;
use sp_core::U256;
use sp_std::vec::Vec;
use sp_io::crypto::secp256k1_ecdsa_recover;
use sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, UniqueSaturatedInto};
use ghost_helpers::{
SubstrateKeccakHasher, merkle_tree::verify_tree_proof,
networks::{NetworkData, NetworkCurve, NetworkType, NetworkClaim},
};
use ghost_traits::{
hashing::GhostHasher,
networks::{
NetworkDataBasicHandler, NetworkDataInspectHandler,
},
};
pub use pallet::*;
mod weights;
pub use weights::WeightInfo;
mod benchmarking;
mod mock;
mod tests;
const MAX_POSSIBLE_PROOF_DEPTH: u32 = 256;
type TokenId = u128;
type EvmHash = sp_core::H256;
type EvmAddress = sp_core::H160;
type EvmSignature = [u8; 65];
#[derive(Default, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct ShareState<Balance>
where
Balance: Default + Copy + Clone,
{
total_shares: Balance,
claimed_shares: Balance,
}
impl<Balance> ShareState<Balance>
where
Balance: AtLeast32BitUnsigned + Default + Copy + Clone,
{
pub fn new(total_shares: Balance) -> Self {
let mut new_state = ShareState::default();
new_state.total_shares = total_shares;
new_state
}
pub fn try_increase_claimed(&mut self, increase_by: Balance) -> Option<Balance> {
let new_claimed_shares = self.claimed_shares
.saturating_add(increase_by);
if new_claimed_shares > self.total_shares {
return None;
}
self.claimed_shares = new_claimed_shares;
Some(new_claimed_shares)
}
}
#[derive(Default, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct NetworkShare<Balance>
where
Balance: Default + Copy + Clone,
{
proofs_size: u32,
merkle_root: EvmHash,
share_state: ShareState<Balance>
}
impl<Balance> NetworkShare<Balance>
where
Balance: AtLeast32BitUnsigned + Default + Copy + Clone,
{
pub fn new(
total_shares: Balance,
merkle_root: EvmHash,
proofs_size: u32
) -> Self {
let share_state = ShareState::new(total_shares);
Self { merkle_root, share_state, proofs_size }
}
pub fn total_shares(&self) -> Balance {
self.share_state.total_shares
}
pub fn claimed_shares(&self) -> Balance {
self.share_state.claimed_shares
}
pub fn try_increase_claimed(&mut self, increase_by: Balance) -> Option<Balance> {
self.share_state.try_increase_claimed(increase_by)
}
}
#[derive(Default, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct MemberShare<Balance, NetworkId>
where
Balance: AtLeast32BitUnsigned + Default + Copy,
NetworkId: Default + Copy,
{
initiated_network: Option<NetworkId>,
activated_shares: Balance,
locked_balance: Balance,
}
impl<Balance, NetworkId> MemberShare<Balance, NetworkId>
where
Balance: AtLeast32BitUnsigned + Default + Copy,
NetworkId: Default + Copy,
{
pub fn increase_activated_shares(&mut self, extra_shares: Balance) {
self.activated_shares = self.activated_shares
.saturating_add(extra_shares);
}
pub fn initiate_on_network(&mut self, network_id: NetworkId) {
self.initiated_network = Some(network_id);
}
pub fn lock_balance(&mut self, extra_balance: Balance) {
self.locked_balance.saturating_add(extra_balance);
}
}
#[derive(Encode, Decode, Clone, RuntimeDebug, TypeInfo, PartialEq, Eq)]
pub struct ClaimPackage<Balance>
where
Balance: Default + Copy + Clone,
{
pub token_id: TokenId,
pub balance: Balance,
pub merkle_proof: BoundedVec<EvmHash, ConstU32<MAX_POSSIBLE_PROOF_DEPTH>>,
}
impl<Balance> ClaimPackage<Balance>
where
Balance: Default + Copy + Clone + UniqueSaturatedInto<u128>,
{
pub fn get_preimage(&self, evm_address: &EvmAddress) -> [u8; 96] {
let mut token_id_bytes = [0u8; 32];
U256::from(self.token_id as u128).to_big_endian(&mut token_id_bytes);
let balance_u128: u128 = self.balance.unique_saturated_into();
let mut balance_bytes = [0u8; 32];
U256::from(balance_u128).to_big_endian(&mut balance_bytes);
let mut address_bytes = [0u8; 32];
address_bytes[12..32].copy_from_slice(evm_address.as_bytes());
let mut preimage = [0u8; 96];
preimage[0..32].copy_from_slice(&token_id_bytes);
preimage[32..64].copy_from_slice(&balance_bytes);
preimage[64..96].copy_from_slice(&address_bytes);
preimage
}
}
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type NetworkIdOf<T> =
<<T as Config>::NetworkDataHandler as NetworkDataBasicHandler>::NetworkId;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + core::fmt::Debug {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: Currency<Self::AccountId>;
type NetworkDataHandler: NetworkDataInspectHandler<NetworkData>
+ NetworkDataBasicHandler<NetworkCurve = NetworkCurve, NetworkType = NetworkType>;
#[pallet::constant]
type MinimumDonation: Get<u128>;
#[pallet::constant]
type MaxProofDepth: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SharesClaimed {
who: T::AccountId,
balance: BalanceOf<T>,
network_id: NetworkIdOf<T>,
}
}
#[pallet::error]
pub enum Error<T> {
PreimageAlreadyClaimed,
BoundedToOtherNetwork,
AuthorityNotGovernor,
NetworkHasNoGenesis,
InvalidMerkleProof,
SharesOverflowed,
BadSignature,
}
#[pallet::storage]
#[pallet::getter(fn global_shares)]
pub type GlobalShares<T: Config> = StorageValue<
_,
ShareState<BalanceOf<T>>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn network_shares)]
pub type NetworkShares<T: Config> = StorageMap<
_,
Twox64Concat, NetworkIdOf<T>,
NetworkShare<BalanceOf<T>>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn member_shares)]
pub type MemberShares<T: Config> = CountedStorageMap<
_,
Blake2_256, T::AccountId,
MemberShare<BalanceOf<T>, NetworkIdOf<T>>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn claimed_hashes)]
pub type ClaimedHashes<T: Config> = StorageMap<
_,
Blake2_256, EvmHash,
bool,
ValueQuery,
>;
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub network_claims: Vec<NetworkClaim<NetworkIdOf<T>, BalanceOf<T>>>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
let mut global_shares: BalanceOf<T> = Default::default();
self.network_claims
.iter()
.for_each(|c| {
assert!(
c.proof_size <= T::MaxProofDepth::get(),
"CRITICAL: constant T::MaxProofDepth is not enough for network {:?}",
c.network_id,
);
let network_state = NetworkShare::new(c.amount, c.merkle_root, c.proof_size);
NetworkShares::<T>::insert(c.network_id, network_state);
global_shares = global_shares.checked_add(&c.amount)
.expect("Global total shares overflowed");
});
let global_state = ShareState::new(global_shares);
GlobalShares::<T>::put(global_state);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::claim())]
pub fn claim(
origin: OriginFor<T>,
network_id: NetworkIdOf<T>,
claim_package: ClaimPackage<BalanceOf<T>>,
evm_address: EvmAddress,
evm_signature: EvmSignature,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut member_share = MemberShares::<T>::get(&who);
let minimum_donation: BalanceOf<T> = T::MinimumDonation::get()
.unique_saturated_into();
ensure!(
member_share.locked_balance >= minimum_donation,
Error::<T>::AuthorityNotGovernor,
);
let correct_network_bound = member_share.initiated_network
.map(|inner_network_id| inner_network_id == network_id)
.unwrap_or(true);
ensure!(correct_network_bound, Error::<T>::BoundedToOtherNetwork);
let preimage = claim_package.get_preimage(&evm_address);
let preimage_hash = SubstrateKeccakHasher::hash(&preimage);
ensure!(
!ClaimedHashes::<T>::contains_key(&preimage_hash),
Error::<T>::PreimageAlreadyClaimed
);
Self::verify_signature(&who, &network_id, &preimage_hash, &evm_address, &evm_signature)?;
Self::verify_merkle_proof(&network_id, &preimage, &claim_package)?;
NetworkShares::<T>::try_mutate(&network_id, |state| -> DispatchResult {
let package_proof_size = claim_package.merkle_proof.len() as u32;
let proof_size_match = package_proof_size == state.proofs_size;
ensure!(proof_size_match, Error::<T>::SharesOverflowed);
let increased = state.try_increase_claimed(claim_package.balance);
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
Ok(())
})?;
GlobalShares::<T>::try_mutate(|state| -> DispatchResult {
let increased = state.try_increase_claimed(claim_package.balance);
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
Ok(())
})?;
member_share.increase_activated_shares(claim_package.balance);
member_share.initiate_on_network(network_id);
MemberShares::<T>::insert(&who, member_share);
ClaimedHashes::<T>::insert(&preimage_hash, true);
Self::deposit_event(Event::<T>::SharesClaimed {
balance: claim_package.balance,
network_id,
who,
});
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
fn to_ascii_hex(data: &[u8]) -> Vec<u8> {
let mut r = Vec::with_capacity(data.len() * 2);
let mut push_nibble = |n| r.push(if n < 10 { b'0' + n } else { b'a' - 10 + n });
for &b in data.iter() {
push_nibble(b / 16);
push_nibble(b % 16);
}
r
}
fn ethereum_signable_message(
receiver: &T::AccountId,
network_id: &NetworkIdOf<T>,
preimage_hash: &EvmHash,
) -> Vec<u8> {
let prefix = b"GMV Claim:";
let network_id_u64: u64 = (*network_id).unique_saturated_into();
let receiver_ascii_hex = Self::to_ascii_hex(&receiver.encode());
let preimage_ascii_hex = Self::to_ascii_hex(preimage_hash.as_ref());
let network_ascii_hex = Self::to_ascii_hex(&network_id_u64.to_be_bytes());
let what_length = receiver_ascii_hex.len()
.saturating_add(preimage_ascii_hex.len())
.saturating_add(network_ascii_hex.len())
.saturating_add(10 + 11 + 12 + 11); // all 4 prefixes
let mut what = Vec::with_capacity(what_length);
what.extend_from_slice(prefix);
what.extend_from_slice(b"\naccount:0x");
what.extend_from_slice(&receiver_ascii_hex);
what.extend_from_slice(b"\npreimage:0x");
what.extend_from_slice(&preimage_ascii_hex);
what.extend_from_slice(b"\nnetwork:0x");
what.extend_from_slice(&network_ascii_hex);
let mut l = what.len();
let mut rev = Vec::new();
while l > 0 {
rev.push(b'0' + (l % 10) as u8);
l /= 10;
}
let mut v = Vec::with_capacity(26 + rev.len() + what.len());
v.extend_from_slice(b"\x19Ethereum Signed Message:\n");
v.extend(rev.into_iter().rev());
v.extend(what);
v
}
fn verify_signature(
who: &T::AccountId,
network_id: &NetworkIdOf<T>,
preimage_hash: &EvmHash,
evm_address: &EvmAddress,
evm_signature: &EvmSignature,
) -> DispatchResult {
let message = Self::ethereum_signable_message(who, network_id, preimage_hash);
let message_bytes = SubstrateKeccakHasher::hash(&message).to_fixed_bytes();
let recovered_pubkey = secp256k1_ecdsa_recover(evm_signature, &message_bytes)
.map_err(|_| Error::<T>::BadSignature)?;
let recovered_pubkey_hash = SubstrateKeccakHasher::hash(&recovered_pubkey);
let recovered_address = EvmAddress::from_slice(&recovered_pubkey_hash[12..32]);
ensure!(recovered_address == *evm_address, Error::<T>::BadSignature);
Ok(())
}
fn verify_merkle_proof(
network_id: &NetworkIdOf<T>,
preimage: &[u8],
claim_package: &ClaimPackage<BalanceOf<T>>,
) -> DispatchResult {
let network_state = NetworkShares::<T>::get(network_id);
let is_merkle_proof_valid =
verify_tree_proof::<SubstrateKeccakHasher, TokenId>(
preimage,
&claim_package.merkle_proof,
network_state.merkle_root,
claim_package.token_id,
);
ensure!(is_merkle_proof_valid, Error::<T>::InvalidMerkleProof);
Ok(())
}
}

View File

@ -1,87 +0,0 @@
#![cfg(test)]
use super::*;
pub use crate as ghost_governance;
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU128},
};
use frame_system::EnsureRoot;
use sp_runtime::BuildStorage;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type AccountData = pallet_balances::AccountData<u64>;
type MaxConsumers = frame_support::traits::ConstU32<16>;
type AccountId = sp_runtime::AccountId32;
type Lookup = sp_runtime::traits::IdentityLookup<Self::AccountId>;
}
#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl pallet_balances::Config for Test {
type AccountStore = System;
}
parameter_types! {
pub const MaxNetworks: u32 = 5;
}
impl ghost_networks::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type NetworkId = u64;
type RegisterOrigin = EnsureRoot<Self::AccountId>;
type UpdateOrigin = EnsureRoot<Self::AccountId>;
type RemoveOrigin = EnsureRoot<Self::AccountId>;
type MaxNetworks = MaxNetworks;
type WeightInfo = ();
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type NetworkDataHandler = Networks;
type MinimumDonation = ConstU128<69>;
type MaxProofDepth = ConstU32<5>;
type WeightInfo = ();
}
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Balances: pallet_balances,
Networks: ghost_networks,
Governance: ghost_governance,
}
);
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
ghost_governance::GenesisConfig::<Test> {
network_claims: Default::default(),
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| {
System::set_block_number(1);
});
ext
}

View File

@ -1,170 +0,0 @@
#![cfg(test)]
use mock::*;
use super::*;
use sp_runtime::AccountId32;
use frame_support::traits::Get;
use frame_support::assert_ok;
use libsecp256k1::{Message, SecretKey, PublicKey};
use ghost_helpers::merkle_tree::{generate_tree, generate_proof};
#[derive(Clone)]
pub struct NewTestUser {
pub substrate_account: AccountId32,
pub evm_secret: SecretKey,
pub evm_address: EvmAddress,
pub allowed_balance: u64,
pub token_id: u128,
}
#[test]
fn claim_happy_path() {
new_test_ext().execute_with(|| {
let network_id = 69u64;
let minimum_donation_u128 = <<Test as crate::Config>::MinimumDonation as Get<u128>>::get();
let minimum_donation: BalanceOf<Test> = minimum_donation_u128.unique_saturated_into();
let secret_key_1 = SecretKey::parse(&[1u8; 32]).unwrap();
let public_key_1 = PublicKey::from_secret_key(&secret_key_1);
let pubkey_raw_1 = &public_key_1.serialize()[1..65];
let pubkey_hash_1 = SubstrateKeccakHasher::hash(pubkey_raw_1);
let mut evm_addr_bytes_1 = [0u8; 20];
evm_addr_bytes_1.copy_from_slice(&pubkey_hash_1.as_ref()[12..32]);
let user_1 = NewTestUser {
substrate_account: AccountId32::new([11u8; 32]),
evm_secret: secret_key_1,
evm_address: EvmAddress::from(evm_addr_bytes_1),
allowed_balance: 420u64,
token_id: 0,
};
let secret_key_2 = SecretKey::parse(&[2u8; 32]).unwrap();
let public_key_2 = PublicKey::from_secret_key(&secret_key_2);
let pubkey_raw_2 = &public_key_2.serialize()[1..65];
let pubkey_hash_2 = SubstrateKeccakHasher::hash(pubkey_raw_2);
let mut evm_addr_bytes_2 = [0u8; 20];
evm_addr_bytes_2.copy_from_slice(&pubkey_hash_2.as_ref()[12..32]);
let user_2 = NewTestUser {
substrate_account: AccountId32::new([22u8; 32]),
evm_secret: secret_key_2,
evm_address: EvmAddress::from(evm_addr_bytes_2),
allowed_balance: 1337u64,
token_id: 1,
};
let secret_key_3 = SecretKey::parse(&[3u8; 32]).unwrap();
let public_key_3 = PublicKey::from_secret_key(&secret_key_3);
let pubkey_raw_3 = &public_key_3.serialize()[1..65];
let pubkey_hash_3 = SubstrateKeccakHasher::hash(pubkey_raw_3);
let mut evm_addr_bytes_3 = [0u8; 20];
evm_addr_bytes_3.copy_from_slice(&pubkey_hash_3.as_ref()[12..32]);
let user_3 = NewTestUser {
substrate_account: AccountId32::new([69u8; 32]),
evm_secret: secret_key_3,
evm_address: EvmAddress::from(evm_addr_bytes_3),
allowed_balance: 69u64,
token_id: 2,
};
let users = vec![user_1.clone(), user_2.clone(), user_3.clone()];
let max_index = 2u8;
let raw_values_complex = vec![(0u8, &user_1), (1u8, &user_2), (2u8, &user_3)];
let merkle_tree = generate_tree::<SubstrateKeccakHasher, u8, _, _, _>(
max_index,
raw_values_complex,
|item: (u8, &NewTestUser)| -> Result<(usize, Vec<u8>), ()> {
let (index, u) = item;
let tmp_package = ClaimPackage {
balance: u.allowed_balance,
merkle_proof: Default::default(),
token_id: u.token_id,
};
Ok((index as usize, tmp_package.get_preimage(&u.evm_address).to_vec()))
}
).unwrap();
let merkle_root = *merkle_tree.last().unwrap();
let network_state = NetworkShare::new(1_000_000, merkle_root, 2);
let global_state = ShareState::new(1_000_000);
NetworkShares::<Test>::insert(&network_id, network_state);
GlobalShares::<Test>::put(global_state);
let mut global_activated = GlobalShares::<Test>::get().claimed_shares;
let mut network_activated = NetworkShares::<Test>::get(&network_id)
.claimed_shares();
for (slot_index, current_user) in users.iter().enumerate() {
MemberShares::<Test>::insert(&current_user.substrate_account, MemberShare {
locked_balance: minimum_donation,
initiated_network: None,
activated_shares: 0,
});
let proof = generate_proof::<SubstrateKeccakHasher, u8>(&merkle_tree, max_index, slot_index as u8);
let claim_package = ClaimPackage {
balance: current_user.allowed_balance,
merkle_proof: BoundedVec::try_from(proof).unwrap(),
token_id: current_user.token_id,
};
let preimage = claim_package.get_preimage(&current_user.evm_address);
let preimage_hash = SubstrateKeccakHasher::hash(&preimage);
let message_string_bytes = Governance::ethereum_signable_message(
&current_user.substrate_account,
&network_id,
&preimage_hash
);
let message_hash = SubstrateKeccakHasher::hash(&message_string_bytes);
let mut message_bytes = [0u8; 32];
message_bytes.copy_from_slice(message_hash.as_ref());
let msg_to_sign = Message::parse(&message_bytes);
let (sig, recovery_id) = libsecp256k1::sign(&msg_to_sign, &current_user.evm_secret);
let mut signature_raw = [0u8; 65];
signature_raw[0..32].copy_from_slice(&sig.r.b32());
signature_raw[32..64].copy_from_slice(&sig.s.b32());
signature_raw[64] = recovery_id.serialize();
let evm_signature = EvmSignature::from(signature_raw);
assert_ok!(Governance::claim(
RuntimeOrigin::signed(current_user.substrate_account.clone()),
network_id,
claim_package,
current_user.evm_address.clone(),
evm_signature
));
let updated_share = MemberShares::<Test>::get(&current_user.substrate_account);
assert_eq!(updated_share.activated_shares, current_user.allowed_balance);
global_activated += current_user.allowed_balance;
network_activated += current_user.allowed_balance;
}
assert_eq!(global_activated, GlobalShares::<Test>::get().claimed_shares);
assert_eq!(
network_activated,
NetworkShares::<Test>::get(&network_id).claimed_shares(),
);
});
}

View File

@ -1,43 +0,0 @@
[package]
name = "ghost-helpers"
version = "0.0.5"
description = "Cryptographic utility suite for custom runtimes: optimized Bitmaps, UTXO parsing, Merkle Tree proofs, and Hash Chain components."
license.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
num-traits = { workspace = true }
strum = { workspace = true, features = ["derive"] }
scale-info = { workspace = true, features = ["derive"] }
codec = { workspace = true, features = ["max-encoded-len"] }
serde = { workspace = true, features = ["derive"] }
frame-support = { workspace = true }
sp-arithmetic = { workspace = true }
sp-runtime = { workspace = true }
sp-core = { workspace = true }
sp-std = { workspace = true }
sp-io = { workspace = true }
ghost-traits = { workspace = true }
[features]
default = ["std"]
std = [
"serde/std",
"num-traits/std",
"strum/std",
"scale-info/std",
"codec/std",
"frame-support/std",
"sp-runtime/std",
"sp-arithmetic/std",
"sp-std/std",
"ghost-traits/std",
"sp-io/std",
"sp-core/std",
]
runtime-benchmarks = []

File diff suppressed because it is too large Load Diff

View File

@ -1,316 +0,0 @@
use ghost_traits::hashing::GhostHasher;
fn combine_values<H, V>(left: V, right: V) -> H::Hash
where
H: GhostHasher,
V: AsRef<[u8]>,
{
let hash_len = H::hash_len();
let mut combined = sp_std::vec![0u8; hash_len * 2];
combined[..hash_len].copy_from_slice(left.as_ref());
combined[hash_len..].copy_from_slice(right.as_ref());
H::hash(&combined)
}
pub fn cumulative_hash<H, S>(raw_values: S) -> H::Hash
where
H: GhostHasher,
S: IntoIterator,
S::Item: AsRef<[u8]>,
{
raw_values
.into_iter()
.fold(H::empty(), |acc_hash, raw_value| {
let hashed_value = H::hash(raw_value.as_ref());
combine_values::<H, _>(acc_hash, hashed_value)
})
}
pub fn sequential_hash<H, S>(raw_values: S) -> H::Hash
where
H: GhostHasher,
S: IntoIterator,
S::Item: AsRef<[u8]>,
{
let mut iter = raw_values.into_iter().peekable();
let Some(first_value) = iter.next() else {
return H::empty();
};
if iter.peek().is_none() {
return H::hash(first_value.as_ref());
}
let current_hash = H::from_slice(first_value.as_ref());
iter.fold(current_hash, |acc_hash, raw_value| {
let next_hash = H::from_slice(raw_value.as_ref());
combine_values::<H, _>(acc_hash, next_hash)
})
}
pub fn verify_hash_ancestry<H, S>(
tx_block_header_bytes: &[u8],
ancestry_headers: S,
trusted_top_block_hash: H::Hash,
) -> bool
where
H: GhostHasher,
H::Hash: PartialEq + Copy + AsRef<[u8]> + AsMut<[u8]>,
S: IntoIterator,
S::Item: AsRef<[u8]>,
{
ancestry_headers
.into_iter()
.try_fold(
sequential_hash::<H, _>(core::iter::once(tx_block_header_bytes)),
|acc_parent_hash, current_header| {
let header_ref = current_header.as_ref();
if header_ref.len() < 36 {
return None;
}
let mut current_prev_hash = [0u8; 32];
current_prev_hash.copy_from_slice(&header_ref[4..36]);
if current_prev_hash != acc_parent_hash.as_ref() {
return None;
}
let current_header_iter = core::iter::once(header_ref);
Some(sequential_hash::<H, _>(current_header_iter))
},
)
.map(|mut final_le_hash| {
final_le_hash.as_mut().reverse();
final_le_hash == trusted_top_block_hash
})
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use sp_core::H256;
use sp_io::hashing::{blake2_256, sha2_256};
use sp_std::vec::Vec;
pub struct TestBlake2Hasher;
impl GhostHasher for TestBlake2Hasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
H256::from(blake2_256(data))
}
fn empty() -> Self::Hash {
H256::zero()
}
fn hash_len() -> usize {
H256::len_bytes()
}
}
pub struct TestSha2Hasher;
impl GhostHasher for TestSha2Hasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
let first_pass: [u8; 32] = sha2_256(data);
let second_pass: [u8; 32] = sha2_256(&first_pass);
H256::from(second_pass)
}
fn hash_len() -> usize {
H256::len_bytes()
}
fn empty() -> Self::Hash {
H256::zero()
}
}
fn mock_sequential_hash(header: &[u8; 80]) -> H256 {
sequential_hash::<TestSha2Hasher, _>(core::iter::once(header))
}
fn create_header(prev_hash: [u8; 32]) -> [u8; 80] {
let mut header = [0u8; 80];
header[4..36].copy_from_slice(&prev_hash);
header
}
#[test]
fn test_generate_empty_values() {
let empty_list: Vec<Vec<u8>> = sp_std::vec![];
let result = cumulative_hash::<TestBlake2Hasher, _>(empty_list);
assert_eq!(H256::zero(), result);
}
#[test]
fn test_cumulative_hash_determinism_and_avalanche() {
let dataset_1 = sp_std::vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let dataset_2 = sp_std::vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let dataset_3 = sp_std::vec![vec![9, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
let hash_1 = cumulative_hash::<TestBlake2Hasher, _>(dataset_1);
let hash_2 = cumulative_hash::<TestBlake2Hasher, _>(dataset_2);
let hash_3 = cumulative_hash::<TestBlake2Hasher, _>(dataset_3);
assert_eq!(hash_1, hash_2);
assert_ne!(hash_1, hash_3);
assert_ne!(hash_2, hash_3);
}
#[test]
fn test_cumulative_hash_order_sensitive() {
let dataset_forward = sp_std::vec![sp_std::vec![1, 1, 1], sp_std::vec![2, 2, 2]];
let dataset_reversed = sp_std::vec![sp_std::vec![2, 2, 2], sp_std::vec![1, 1, 1]];
let hash_forward = cumulative_hash::<TestBlake2Hasher, _>(dataset_forward);
let hash_reversed = cumulative_hash::<TestBlake2Hasher, _>(dataset_reversed);
assert_ne!(hash_forward, hash_reversed);
}
#[test]
fn test_cumulative_hash_with_slices() {
let slice_data: &[&[u8]] = &[&[1, 2, 3], &[4, 5, 6]];
let hash_from_slice = cumulative_hash::<TestBlake2Hasher, _>(slice_data.iter());
assert_ne!(hash_from_slice, H256::zero());
}
#[test]
fn test_sequential_hash_empty() {
let empty_list: Vec<Vec<u8>> = sp_std::vec![];
let result = sequential_hash::<TestBlake2Hasher, _>(empty_list);
assert_eq!(H256::zero(), result);
}
#[test]
fn test_sequential_hash_single_element() {
let single_item = sp_std::vec![sp_std::vec![1, 2, 3]];
let result = sequential_hash::<TestBlake2Hasher, _>(single_item);
let expected = TestBlake2Hasher::hash(&[1, 2, 3]);
assert_eq!(result, expected);
}
#[test]
fn test_sequential_hash_determinism_and_order() {
let data_1 = sp_std::vec![sp_std::vec![1], sp_std::vec![2], sp_std::vec![3]];
let data_2 = sp_std::vec![sp_std::vec![1], sp_std::vec![2], sp_std::vec![3]];
let data_reversed = sp_std::vec![sp_std::vec![3], sp_std::vec![2], sp_std::vec![1]];
let hash_1 = sequential_hash::<TestBlake2Hasher, _>(data_1);
let hash_2 = sequential_hash::<TestBlake2Hasher, _>(data_2);
let hash_reversed = sequential_hash::<TestBlake2Hasher, _>(data_reversed);
assert_eq!(hash_1, hash_2);
assert_ne!(hash_1, hash_reversed);
}
#[test]
fn test_sequential_vs_cumulative_hash() {
let dataset = sp_std::vec![sp_std::vec![1, 2, 3], sp_std::vec![4, 5, 6]];
let seq_hash = sequential_hash::<TestBlake2Hasher, _>(dataset.clone());
let cum_hash = cumulative_hash::<TestBlake2Hasher, _>(dataset);
assert_ne!(seq_hash, cum_hash);
}
#[test]
fn test_sequential_hash_with_slices() {
let slice_data: &[&[u8]] = &[&[10, 20], &[30, 40]];
let result = sequential_hash::<TestBlake2Hasher, _>(slice_data.iter());
assert_ne!(result, H256::zero());
}
#[test]
fn test_verify_in_current_block_with_empty_ancestry() {
let tx_header = [0x11u8; 80];
let tx_block_le_hash = mock_sequential_hash(&tx_header);
let mut trusted_bytes = tx_block_le_hash.0;
trusted_bytes.reverse();
let trusted_top_block_hash = H256::from(trusted_bytes);
let result = verify_hash_ancestry::<TestSha2Hasher, &[[u8; 80]]>(
&tx_header,
&[],
trusted_top_block_hash,
);
assert!(result);
}
#[test]
fn test_verify_with_single_ancestor_header() {
let tx_header_100 = [0x22u8; 80];
let hash_100_le = mock_sequential_hash(&tx_header_100);
let header_101 = create_header(hash_100_le.0);
let hash_101_le = mock_sequential_hash(&header_101);
let mut trusted_bytes = hash_101_le.0;
trusted_bytes.reverse();
let trusted_top_block_hash = H256::from(trusted_bytes);
let ancestry = [header_101];
let result = verify_hash_ancestry::<TestSha2Hasher, &[[u8; 80]]>(
&tx_header_100,
&ancestry,
trusted_top_block_hash,
);
assert!(result);
}
#[test]
fn test_verify_with_multiple_ancestor_headers_chain() {
let header_100 = [0x55u8; 80];
let hash_100_le = mock_sequential_hash(&header_100);
let header_101 = create_header(hash_100_le.0);
let hash_101_le = mock_sequential_hash(&header_101);
let header_102 = create_header(hash_101_le.0);
let hash_102_le = mock_sequential_hash(&header_102);
let header_103 = create_header(hash_102_le.0);
let hash_103_le = mock_sequential_hash(&header_103);
let mut trusted_bytes = hash_103_le.0;
trusted_bytes.reverse();
let trusted_top_block_hash = H256::from(trusted_bytes);
let ancestry = [header_101, header_102, header_103];
let result = verify_hash_ancestry::<TestSha2Hasher, &[[u8; 80]]>(
&header_100,
&ancestry,
trusted_top_block_hash,
);
assert!(result);
}
#[test]
fn test_verify_fails_if_chain_is_broken() {
let header_100 = [0xAAu8; 80];
let broken_header_101 = create_header([0xFF; 32]);
let hash_101_le = mock_sequential_hash(&broken_header_101);
let mut trusted_bytes = hash_101_le.0;
trusted_bytes.reverse();
let trusted_top_block_hash = H256::from(trusted_bytes);
let ancestry = [broken_header_101];
let result = verify_hash_ancestry::<TestSha2Hasher, &[[u8; 80]]>(
&header_100,
&ancestry,
trusted_top_block_hash,
);
assert!(!result);
}
}

View File

@ -1,99 +0,0 @@
#![cfg_attr(not(feature = "std"), no_std)]
use ghost_traits::hashing::GhostHasher;
use sp_core::H256;
use sp_io::hashing::{blake2_256, keccak_256, sha2_256};
pub mod bounded_bitmap;
pub mod hash_chain;
pub mod merkle_tree;
pub mod networks;
pub mod utxo;
pub type AuthIndexU8 = u8;
pub type AuthIndexU16 = u16;
pub type AuthIndexU32 = u32;
pub type AuthIndexU64 = u64;
pub type AuthIndexU128 = u128;
pub type BitmapChunkU8 = u8;
pub type BitmapChunkU16 = u16;
pub type BitmapChunkU32 = u32;
pub type BitmapChunkU64 = u64;
pub type BitmapChunkU128 = u128;
pub type DkgIndexU8 = u8;
pub type DkgIndexU16 = u16;
pub type DkgIndexU32 = u32;
pub type DkgIndexU64 = u64;
pub type DkgIndexU128 = u128;
pub type NetworkIdU8 = u8;
pub type NetworkIdU16 = u16;
pub type NetworkIdU32 = u32;
pub type NetworkIdU64 = u64;
pub type NetworkIdU128 = u128;
pub fn get_byzantium_threshold<I>(value: I) -> I
where
I: num_traits::PrimInt,
{
let zero = I::zero();
let one = I::one();
let two = one + one;
let three = two + one;
if value == zero {
return zero;
}
(value - one) / three * two + one
}
pub struct SubstrateBlake2Hasher;
impl GhostHasher for SubstrateBlake2Hasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
H256::from(blake2_256(data))
}
fn hash_len() -> usize {
H256::len_bytes()
}
fn empty() -> Self::Hash {
H256::zero()
}
}
pub struct SubstrateKeccakHasher;
impl GhostHasher for SubstrateKeccakHasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
H256::from(keccak_256(data))
}
fn hash_len() -> usize {
H256::len_bytes()
}
fn empty() -> Self::Hash {
H256::zero()
}
}
pub struct UtxoSha2Hasher;
impl GhostHasher for UtxoSha2Hasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
let first_pass: [u8; 32] = sha2_256(data);
let second_pass: [u8; 32] = sha2_256(&first_pass);
H256::from(second_pass)
}
fn hash_len() -> usize {
H256::len_bytes()
}
fn empty() -> Self::Hash {
H256::zero()
}
}

View File

@ -1,571 +0,0 @@
use ghost_traits::hashing::GhostHasher;
use sp_arithmetic::traits::AtLeast8BitUnsigned;
use sp_runtime::traits::UniqueSaturatedInto;
use sp_std::vec::Vec;
pub fn generate_tree<H, I, S, F, E>(
max_index: I,
raw_values: S,
generate_preimage: F,
) -> Result<Vec<H::Hash>, E>
where
H: GhostHasher,
S: IntoIterator,
I: AtLeast8BitUnsigned + UniqueSaturatedInto<usize>,
F: Fn(S::Item) -> Result<(usize, Vec<u8>), E>,
{
let num_of_leaves: usize = (max_index + I::one()).unique_saturated_into();
let padded_leaves = num_of_leaves.next_power_of_two();
let total_capacity = (2 * padded_leaves) - 1;
let mut merkle_tree = sp_std::vec![H::empty(); total_capacity];
for item in raw_values.into_iter() {
let (index, preimage) = generate_preimage(item)?;
merkle_tree[index] = H::hash(&preimage);
}
let mut layer_start = 0;
let mut current_layer_len = padded_leaves;
let mut write_ptr = padded_leaves;
let hash_len = H::hash_len();
let mut combined = sp_std::vec![0u8; hash_len * 2];
while current_layer_len > 1 {
for i in (0..current_layer_len).step_by(2) {
let layer_index = layer_start + i;
let left_bytes = merkle_tree[layer_index].as_ref();
let right_bytes = merkle_tree[layer_index | 1].as_ref();
combined[..hash_len].copy_from_slice(left_bytes);
combined[hash_len..].copy_from_slice(right_bytes);
merkle_tree[write_ptr] = H::hash(&combined);
write_ptr += 1;
}
layer_start += current_layer_len;
current_layer_len >>= 1;
}
Ok(merkle_tree)
}
pub fn generate_proof<H, I>(merkle_tree: &[H::Hash], max_index: I, index: I) -> Vec<H::Hash>
where
H: GhostHasher,
I: AtLeast8BitUnsigned + UniqueSaturatedInto<usize>,
{
let num_of_leaves: usize = (max_index + I::one()).unique_saturated_into();
let mut current_index: usize = index.unique_saturated_into();
let mut current_layer_len = num_of_leaves.next_power_of_two();
let tree_height = current_layer_len.trailing_zeros() as usize;
let mut proof = Vec::with_capacity(tree_height);
let mut layer_start = 0;
while current_layer_len > 1 {
let sibling_idx_in_layer = current_index ^ 1;
let sibling_hash = merkle_tree[layer_start + sibling_idx_in_layer];
proof.push(sibling_hash);
layer_start += current_layer_len;
current_layer_len >>= 1;
current_index >>= 1;
}
proof
}
pub fn verify_tree_proof<H, I>(
preimage: &[u8],
merkle_proof: &[H::Hash],
merkle_root: H::Hash,
index: I,
) -> bool
where
H: GhostHasher,
I: PartialOrd + UniqueSaturatedInto<usize>,
{
let mut current_hash = H::hash(preimage);
let mut current_index: usize = index.unique_saturated_into();
if current_index >= (1 << merkle_proof.len()) {
return false;
}
let hash_len = H::hash_len();
let mut combined = sp_std::vec![0u8; hash_len * 2];
for sibling in merkle_proof.iter() {
let sibling_bytes = sibling.as_ref();
let hash_bytes = current_hash.as_ref();
if current_index % 2 == 0 {
combined[..hash_len].copy_from_slice(hash_bytes);
combined[hash_len..].copy_from_slice(sibling_bytes);
} else {
combined[..hash_len].copy_from_slice(sibling_bytes);
combined[hash_len..].copy_from_slice(hash_bytes);
}
current_hash = H::hash(&combined);
current_index >>= 1;
}
current_hash == merkle_root
}
#[cfg(test)]
mod tests {
use super::*;
use sp_core::H256;
use sp_io::hashing::blake2_256;
pub struct TestBlake2Hasher;
impl GhostHasher for TestBlake2Hasher {
type Hash = H256;
type HashBytes = [u8; 32];
fn hash(data: &[u8]) -> Self::Hash {
H256::from(blake2_256(data))
}
fn empty() -> Self::Hash {
H256::zero()
}
fn hash_len() -> usize {
H256::len_bytes()
}
}
#[test]
fn test_generate_tree_perfect_power_of_two() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
(3, sp_std::vec![4]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
assert_eq!(tree.len(), 7);
let h0 = TestBlake2Hasher::hash(&vec![1]);
let h1 = TestBlake2Hasher::hash(&vec![2]);
let h2 = TestBlake2Hasher::hash(&vec![3]);
let h3 = TestBlake2Hasher::hash(&vec![4]);
let mut c1 = [0u8; 64];
c1[..32].copy_from_slice(h0.as_ref());
c1[32..].copy_from_slice(h1.as_ref());
let parent_left = TestBlake2Hasher::hash(&c1);
let mut c2 = [0u8; 64];
c2[..32].copy_from_slice(h2.as_ref());
c2[32..].copy_from_slice(h3.as_ref());
let parent_right = TestBlake2Hasher::hash(&c2);
let mut c_root = [0u8; 64];
c_root[..32].copy_from_slice(parent_left.as_ref());
c_root[32..].copy_from_slice(parent_right.as_ref());
let expected_root = TestBlake2Hasher::hash(&c_root);
assert_eq!(tree.last().unwrap(), &expected_root);
}
#[test]
fn test_generate_tree_with_padding_hashing() {
let max_index = 2u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
assert_eq!(tree.len(), 7);
assert_eq!(tree[3], H256::zero());
let h2 = TestBlake2Hasher::hash(&vec![3]);
let h3_empty = H256::zero();
let mut combined_right = [0u8; 64];
combined_right[..32].copy_from_slice(h2.as_ref());
combined_right[32..].copy_from_slice(h3_empty.as_ref());
let expected_parent_right = TestBlake2Hasher::hash(&combined_right);
assert_eq!(tree[5], expected_parent_right);
}
#[test]
fn test_generate_tree_determinism() {
let max_index = 1u32;
let data_1 = sp_std::vec![(0, sp_std::vec![100]), (1, sp_std::vec![200])];
let data_2 = sp_std::vec![(0, sp_std::vec![100]), (1, sp_std::vec![200])];
let data_3 = sp_std::vec![(0, sp_std::vec![101]), (1, sp_std::vec![200])];
let tree_1 =
generate_tree::<TestBlake2Hasher, u32, _, _, ()>(max_index, data_1, |(i, b)| {
Ok((i, b))
})
.unwrap();
let tree_2 =
generate_tree::<TestBlake2Hasher, u32, _, _, ()>(max_index, data_2, |(i, b)| {
Ok((i, b))
})
.unwrap();
let tree_3 =
generate_tree::<TestBlake2Hasher, u32, _, _, ()>(max_index, data_3, |(i, b)| {
Ok((i, b))
})
.unwrap();
assert_eq!(tree_1.last().unwrap(), tree_2.last().unwrap());
assert_ne!(tree_1.last().unwrap(), tree_3.last().unwrap());
}
#[test]
fn test_generate_tree_single_element() {
let max_index = 0u32;
let raw_data = sp_std::vec![(0, sp_std::vec![42])];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
assert_eq!(tree.len(), 1);
assert_eq!(tree[0], TestBlake2Hasher::hash(&vec![42]));
}
#[test]
fn test_proof_and_verification_end_to_end() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
(3, sp_std::vec![4]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
let target_index = 2u32;
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, target_index);
assert_eq!(proof.len(), 2);
let preimage = sp_std::vec![3];
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, target_index);
assert!(is_valid);
}
#[test]
fn test_proof_index_out_of_bounds() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
(3, sp_std::vec![4]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
let target_index = 2u32;
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, target_index);
assert_eq!(proof.len(), 2);
let preimage = sp_std::vec![3];
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, target_index + 1);
assert!(!is_valid);
}
#[test]
fn test_proof_contains_correct_siblings() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![10]),
(1, sp_std::vec![20]),
(2, sp_std::vec![30]),
(3, sp_std::vec![40]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let h0 = tree[0];
let h1 = tree[1];
let h2 = tree[2];
let h3 = tree[3];
let parent_left = tree[4]; // hash(h0 + h1)
let parent_right = tree[5]; // hash(h2 + h3)
let proof_for_0 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 0);
assert_eq!(proof_for_0[0], h1);
assert_eq!(proof_for_0[1], parent_right);
let proof_for_1 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 1);
assert_eq!(proof_for_1[0], h0);
assert_eq!(proof_for_1[1], parent_right);
let proof_for_2 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 2);
assert_eq!(proof_for_2[0], h3);
assert_eq!(proof_for_2[1], parent_left);
let proof_for_3 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 3);
assert_eq!(proof_for_3[0], h2);
assert_eq!(proof_for_3[1], parent_left);
}
#[test]
fn test_proof_for_padded_tree_leaf() {
let max_index = 2u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 2);
assert_eq!(proof[0], H256::zero());
let root = *tree.last().unwrap();
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&sp_std::vec![3], &proof, root, 2);
assert!(is_valid);
}
#[test]
fn test_proof_for_single_element_tree() {
let max_index = 0u32;
let raw_data = sp_std::vec![(0, sp_std::vec![99])];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 0);
assert!(proof.is_empty());
}
#[test]
fn test_verify_tree_proof_with_h0_h3_and_random_values() {
let max_index = 3u32;
let leaf_0 = sp_std::vec![10u8];
let leaf_1 = sp_std::vec![20u8];
let leaf_2 = sp_std::vec![30u8];
let leaf_3 = sp_std::vec![40u8];
let raw_data = sp_std::vec![
(0, leaf_0.clone()),
(1, leaf_1.clone()),
(2, leaf_2.clone()),
(3, leaf_3.clone())
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
let h0 = tree[0];
let h1 = tree[1];
let h2 = tree[2];
let h3 = tree[3];
let proof_for_1 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 1);
assert_eq!(proof_for_1[0], h0);
let is_valid_1 = verify_tree_proof::<TestBlake2Hasher, u32>(&leaf_1, &proof_for_1, root, 1);
assert!(is_valid_1);
let proof_for_2 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 2);
assert_eq!(proof_for_2[0], h3);
let is_valid_2 = verify_tree_proof::<TestBlake2Hasher, u32>(&leaf_2, &proof_for_2, root, 2);
assert!(is_valid_2);
let proof_for_0 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 0);
assert_eq!(proof_for_0[0], h1);
assert!(verify_tree_proof::<TestBlake2Hasher, u32>(
&leaf_0,
&proof_for_0,
root,
0
));
let proof_for_3 = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 3);
assert_eq!(proof_for_3[0], h2);
assert!(verify_tree_proof::<TestBlake2Hasher, u32>(
&leaf_3,
&proof_for_3,
root,
3
));
let mut bad_proof = proof_for_1.clone();
bad_proof[0] = H256::random();
let is_valid_bad_proof =
verify_tree_proof::<TestBlake2Hasher, u32>(&leaf_1, &bad_proof, root, 1);
assert!(!is_valid_bad_proof);
let bad_preimage = vec![99u8];
let is_valid_bad_preimage =
verify_tree_proof::<TestBlake2Hasher, u32>(&bad_preimage, &proof_for_1, root, 1);
assert!(!is_valid_bad_preimage);
}
#[test]
fn test_verify_tree_proof_deep_tree_depth_4() {
let max_index = 15u32;
let mut raw_data = sp_std::vec![];
for i in 0..16 {
raw_data.push((i, sp_std::vec![i as u8]));
}
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
assert_eq!(tree.len(), 31);
let target_index = 11u32;
let preimage = vec![target_index as u8];
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, target_index);
assert_eq!(proof.len(), 4);
let h10 = tree[10];
assert_eq!(proof[0], h10);
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, target_index);
assert!(is_valid);
let is_valid_wrong_index =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, 4);
assert!(!is_valid_wrong_index);
}
#[test]
fn test_verification_index_at_upper_bound() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
(3, sp_std::vec![4]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
let target_index = 3u32;
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, target_index);
assert_eq!(proof.len(), 2);
let preimage = sp_std::vec![4];
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, target_index);
assert!(is_valid);
}
#[test]
fn test_verification_fails_when_index_out_of_bounds() {
let max_index = 3u32;
let raw_data = sp_std::vec![
(0, sp_std::vec![1]),
(1, sp_std::vec![2]),
(2, sp_std::vec![3]),
(3, sp_std::vec![4]),
];
let tree = generate_tree::<TestBlake2Hasher, u32, _, _, ()>(
max_index,
raw_data,
|(index, bytes)| Ok((index, bytes)),
)
.unwrap();
let root = *tree.last().unwrap();
let proof = generate_proof::<TestBlake2Hasher, u32>(&tree, max_index, 3u32);
let target_index = 4u32;
let preimage = sp_std::vec![4];
let is_valid =
verify_tree_proof::<TestBlake2Hasher, u32>(&preimage, &proof, root, target_index);
assert!(!is_valid);
}
}

View File

@ -1,335 +0,0 @@
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use sp_runtime::serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumCount, FromRepr};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
use sp_std::vec::Vec;
use sp_core::H256;
pub const MAX_ENDPOINT_LEN: u32 = 128;
pub const MAX_ENDPOINTS_LEN: u32 = 10;
pub const MAX_GATEKEEPER_LEN: u32 = 65;
pub const MAX_SELECTOR_LEN: u32 = 4;
#[derive(
Default,
Encode,
Decode,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
EnumIter,
EnumCount,
FromRepr,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum NetworkType {
#[default]
Evm = 0,
Utxo = 1,
Unknown = 2,
}
impl NetworkType {
pub fn is_evm(&self) -> bool {
matches!(self, NetworkType::Evm)
}
pub fn is_utxo(&self) -> bool {
matches!(self, NetworkType::Utxo)
}
}
impl core::fmt::Display for NetworkType {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let text = match self {
NetworkType::Evm => "EVM",
NetworkType::Utxo => "UTXO",
NetworkType::Unknown => "Unknown",
};
write!(f, "{}", text)
}
}
#[derive(
Default,
Encode,
Decode,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
EnumIter,
EnumCount,
FromRepr,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum NetworkCurve {
#[default]
Secp256k1 = 0,
Ed25519 = 1,
}
impl core::fmt::Display for NetworkCurve {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let text = match self {
NetworkCurve::Secp256k1 => "secp256k1",
NetworkCurve::Ed25519 => "ed25519"
};
write!(f, "{}", text)
}
}
#[derive(
Encode,
Decode,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
Serialize,
Deserialize,
)]
pub struct NetworkInitiation<NetworkId, Balance> {
pub id: NetworkId,
pub amount: Balance,
pub data: NetworkData,
}
#[derive(Default)]
pub struct NetworkInitiationBuilder<NetworkId, Balance>
where
NetworkId: Default,
Balance: Default,
{
pub id: Option<NetworkId>,
pub amount: Option<Balance>,
pub data: Option<NetworkData>,
}
impl<NetworkId, Balance> NetworkInitiationBuilder<NetworkId, Balance>
where
NetworkId: Default,
Balance: Default,
{
pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
self.id = Some(network_id);
self
}
pub fn with_gatekeeper_amount(mut self, amount: Balance) -> Self {
self.amount = Some(amount);
self
}
pub fn with_network_data(mut self, data: NetworkData) -> Self {
self.data = Some(data);
self
}
pub fn build(self) -> NetworkInitiation<NetworkId, Balance> {
NetworkInitiation {
id: self.id.unwrap(),
amount: self.amount.unwrap(),
data: self.data.unwrap(),
}
}
}
#[derive(
Encode,
Decode,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
Serialize,
Deserialize,
)]
pub struct NetworkClaim<NetworkId, Balance> {
pub network_id: NetworkId,
pub merkle_root: H256,
pub amount: Balance,
pub proof_size: u32
}
impl<NetworkId, Balance> NetworkClaim<NetworkId, Balance> {
pub fn with_amount(mut self, amount: Balance) -> Self {
self.amount = amount;
self
}
pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
self.network_id = network_id;
self
}
pub fn with_merkle_root(mut self, merkle_root: H256) -> Self {
self.merkle_root = merkle_root;
self
}
pub fn with_proof_size(mut self, proof_size: u32) -> Self {
self.proof_size = proof_size;
self
}
}
#[derive(
Encode,
Decode,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
Serialize,
Deserialize,
)]
pub struct NetworkData {
pub gatekeeper: BoundedVec<u8, ConstU32<MAX_GATEKEEPER_LEN>>,
pub selector: BoundedVec<u8, ConstU32<MAX_SELECTOR_LEN>>,
pub default_endpoints:
BoundedVec<BoundedVec<u8, ConstU32<MAX_ENDPOINT_LEN>>, ConstU32<MAX_ENDPOINTS_LEN>>,
pub r#type: NetworkType,
pub curve: NetworkCurve,
pub finality_delay: u64,
pub rate_limit_delay: u64,
pub block_deviation: u64,
pub incoming_fee: u32,
pub outgoing_fee: u32,
}
#[derive(Default)]
pub struct NetworkDataBuilder {
pub gatekeeper: Vec<u8>,
pub selector: Vec<u8>,
pub default_endpoints: Vec<Vec<u8>>,
pub r#type: NetworkType,
pub curve: NetworkCurve,
pub finality_delay: u64,
pub rate_limit_delay: u64,
pub block_deviation: u64,
pub incoming_fee: u32,
pub outgoing_fee: u32,
}
impl NetworkDataBuilder {
pub fn with_gatekeeper(mut self, gatekeeper: Vec<u8>) -> Self {
self.gatekeeper = gatekeeper;
self
}
pub fn with_selector(mut self, selector: Vec<u8>) -> Self {
self.selector = selector;
self
}
pub fn with_network_type(mut self, network_type: NetworkType) -> Self {
self.r#type = network_type;
self
}
pub fn with_network_curve(mut self, curve: NetworkCurve) -> Self {
self.curve = curve;
self
}
pub fn with_finality_delay(mut self, finality_delay: u64) -> Self {
self.finality_delay = finality_delay;
self
}
pub fn with_rate_limit_delay(mut self, rate_limit_delay: u64) -> Self {
self.rate_limit_delay = rate_limit_delay;
self
}
pub fn with_block_deviation(mut self, block_deviation: u64) -> Self {
self.block_deviation = block_deviation;
self
}
pub fn with_incoming_fee(mut self, incoming_fee: u32) -> Self {
self.incoming_fee = incoming_fee;
self
}
pub fn with_outgoing_fee(mut self, outgoing_fee: u32) -> Self {
self.outgoing_fee = outgoing_fee;
self
}
pub fn with_default_endpoints(mut self, default_endpoints: Vec<Vec<u8>>) -> Self {
self.default_endpoints = default_endpoints;
self
}
pub fn build(self) -> NetworkData {
let gatekeeper = BoundedVec::<u8, ConstU32<MAX_GATEKEEPER_LEN>>::try_from(self.gatekeeper)
.expect("Gatekeeper exceeds max length");
let selector = BoundedVec::<u8, ConstU32<MAX_SELECTOR_LEN>>::try_from(self.selector)
.expect("Selector exceeds max length");
let default_endpoints = BoundedVec::<
BoundedVec<u8, ConstU32<MAX_ENDPOINT_LEN>>,
ConstU32<MAX_ENDPOINTS_LEN>,
>::try_from(
self.default_endpoints
.into_iter()
.map(|endpoint| {
BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(endpoint)
.expect("Endpoint exceeds max length")
})
.collect::<Vec<BoundedVec<u8, ConstU32<MAX_ENDPOINT_LEN>>>>(),
)
.expect("Too many endpoints");
NetworkData {
gatekeeper,
selector,
default_endpoints,
r#type: self.r#type,
curve: self.curve,
finality_delay: self.finality_delay,
rate_limit_delay: self.rate_limit_delay,
block_deviation: self.block_deviation,
incoming_fee: self.incoming_fee,
outgoing_fee: self.outgoing_fee,
}
}
}

View File

@ -1,745 +0,0 @@
use ghost_traits::hashing::GhostHasher;
use sp_std::vec::Vec;
const MAX_INPUTS: usize = 3;
const MAX_OUTPUTS: usize = 4;
const MAX_SCRIPT_LEN: usize = 34;
const MAX_WITNESS_STACK_DEPTH: usize = 3;
const MAX_WITNESS_ITEMS_LEN: usize = 73;
pub const MAX_TX_LEN: usize = 512;
pub struct TxIn<H: GhostHasher> {
pub prev_txid: H::Hash,
pub vout: u32,
pub script_sig: Vec<u8>,
pub sequence: u32,
}
pub struct TxOut {
pub value: u64,
pub script_pubkey: Vec<u8>,
}
pub struct Witness {
pub stack: Vec<Vec<u8>>,
}
pub enum Transaction<H: GhostHasher> {
Legacy {
version: i32,
inputs: Vec<TxIn<H>>,
outputs: Vec<TxOut>,
lock_time: u32,
},
SegWit {
version: i32,
inputs: Vec<TxIn<H>>,
outputs: Vec<TxOut>,
witnesses: Vec<Witness>,
lock_time: u32,
},
}
pub struct Header<H: GhostHasher> {
pub merkle_root: H::Hash,
pub tx_count: u32,
pub tree_hashes: Vec<H::Hash>,
pub flags: Vec<u8>,
}
impl<H: GhostHasher> Header<H> {
fn calc_tree_width(&self, h: u32) -> u32 {
(self.tx_count + (1 << h) - 1) >> h
}
fn traverse(
&self,
height: u32,
pos: u32,
bits_used: &mut u32,
hash_used: &mut u32,
matched_index: &mut Option<usize>,
target_txid: H::Hash,
) -> Result<H::Hash, ParseError> {
let byte_idx = (*bits_used / 8) as usize;
let bit_idx = (*bits_used % 8) as u8;
if byte_idx >= self.flags.len() {
return Err(ParseError::InvalidVarint(VarintError::BufferTooShort));
}
let parent_of_match = ((self.flags[byte_idx] >> bit_idx) & 1) == 1;
*bits_used += 1;
if height == 0 || !parent_of_match {
if *hash_used as usize >= self.tree_hashes.len() {
return Err(ParseError::InvalidLength);
}
let curr_hash = self.tree_hashes[*hash_used as usize];
*hash_used += 1;
if height == 0 && parent_of_match && curr_hash == target_txid {
*matched_index = Some(pos as usize);
}
return Ok(curr_hash);
}
let left = self.traverse(
height - 1,
pos * 2,
bits_used,
hash_used,
matched_index,
target_txid,
)?;
let right = if pos * 2 + 1 < self.calc_tree_width(height - 1) {
let right = self.traverse(
height - 1,
pos * 2 + 1,
bits_used,
hash_used,
matched_index,
target_txid,
)?;
if right == left {
return Err(ParseError::TypeConversionFailed);
}
right
} else {
left
};
let mut concat = [0u8; 64];
concat[..32].copy_from_slice(left.as_ref());
concat[32..].copy_from_slice(right.as_ref());
let current_node_hash = H::hash(&concat);
Ok(current_node_hash)
}
pub fn extract_proof(&self, target_txid: H::Hash) -> Result<H::Hash, ParseError> {
let mut bits_used = 0u32;
let mut hash_used = 0u32;
let mut matched_index = None;
let mut max_height = 0;
while (1 << max_height) < self.tx_count {
max_height += 1;
}
let computed_root = self.traverse(
max_height,
0,
&mut bits_used,
&mut hash_used,
&mut matched_index,
target_txid,
)?;
if hash_used as usize != self.tree_hashes.len() {
return Err(ParseError::InvalidLength);
}
if (bits_used + 7) / 8 != self.flags.len() as u32 {
return Err(ParseError::InvalidLength);
}
if matched_index.is_some() {
Ok(computed_root)
} else {
Err(ParseError::TransactionNotFoundInProof)
}
}
}
impl<H> Transaction<H>
where
H: GhostHasher<HashBytes = [u8; 32]>,
{
pub fn compute_txid(&self) -> Option<H::Hash> {
let (version, inputs, outputs, lock_time) = match self {
Self::Legacy {
version,
inputs,
outputs,
lock_time,
} => (version, inputs, outputs, lock_time),
Self::SegWit {
version,
inputs,
outputs,
lock_time,
..
} => (version, inputs, outputs, lock_time),
};
let version_bytes = version.to_le_bytes();
let lock_time_bytes = lock_time.to_le_bytes();
let mut tx_buffer = [0u8; MAX_TX_LEN];
let mut cursor = 0;
tx_buffer[cursor..cursor + 4].copy_from_slice(&version_bytes);
cursor += 4;
cursor = Self::serialize_inputs_and_outputs(&mut tx_buffer, cursor, inputs, outputs)?;
if cursor + 4 > MAX_TX_LEN {
return None;
}
tx_buffer[cursor..cursor + 4].copy_from_slice(&lock_time_bytes);
cursor += 4;
let final_tx_bytes = &tx_buffer[..cursor];
Some(H::hash(&final_tx_bytes))
}
fn serialize_inputs_and_outputs(
buffer: &mut [u8; MAX_TX_LEN],
cursor: usize,
inputs: &[TxIn<H>],
outputs: &[TxOut],
) -> Option<usize> {
let mut cursor = Self::write_varint_to_buf(buffer, cursor, inputs.len() as u64)?;
for input in inputs {
if cursor + 32 > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + 32].copy_from_slice(input.prev_txid.as_ref());
cursor += 32;
if cursor + 4 > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + 4].copy_from_slice(&input.vout.to_le_bytes());
cursor += 4;
cursor = Self::write_varint_to_buf(buffer, cursor, input.script_sig.len() as u64)?;
if cursor + input.script_sig.len() > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + input.script_sig.len()].copy_from_slice(&input.script_sig);
cursor += input.script_sig.len();
if cursor + 4 > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + 4].copy_from_slice(&input.sequence.to_le_bytes());
cursor += 4;
}
cursor = Self::write_varint_to_buf(buffer, cursor, outputs.len() as u64)?;
for output in outputs {
if cursor + 8 > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + 8].copy_from_slice(&output.value.to_le_bytes());
cursor += 8;
cursor = Self::write_varint_to_buf(buffer, cursor, output.script_pubkey.len() as u64)?;
if cursor + output.script_pubkey.len() > MAX_TX_LEN {
return None;
}
buffer[cursor..cursor + output.script_pubkey.len()]
.copy_from_slice(&output.script_pubkey);
cursor += output.script_pubkey.len();
}
Some(cursor)
}
fn write_varint_to_buf(
buffer: &mut [u8; MAX_TX_LEN],
cursor: usize,
value: u64,
) -> Option<usize> {
let mut cursor = cursor;
if value < 253 {
if cursor + 1 > MAX_TX_LEN {
return None;
}
buffer[cursor] = value as u8;
cursor += 1;
} else if value <= 0xffff {
if cursor + 3 > MAX_TX_LEN {
return None;
}
buffer[cursor] = 253;
buffer[cursor + 1..cursor + 3].copy_from_slice(&(value as u16).to_le_bytes());
cursor += 3;
} else if value <= 0xffffffff {
if cursor + 5 > MAX_TX_LEN {
return None;
}
buffer[cursor] = 254;
buffer[cursor + 1..cursor + 5].copy_from_slice(&(value as u32).to_le_bytes());
cursor += 5;
} else {
if cursor + 9 > MAX_TX_LEN {
return None;
}
buffer[cursor] = 255;
buffer[cursor + 1..cursor + 9].copy_from_slice(&value.to_le_bytes());
cursor += 9;
}
Some(cursor)
}
pub fn validate<I, A>(
&self,
o_index_i: I,
w_index_i: I,
destination: &[u8],
expected_amount_a: A,
) -> Option<[u8; 32]>
where
I: sp_runtime::traits::UniqueSaturatedInto<usize>,
A: sp_runtime::traits::UniqueSaturatedInto<u128>,
{
let o_index: usize = o_index_i.unique_saturated_into();
let w_index: usize = w_index_i.unique_saturated_into();
let expected_amount_u128: u128 = expected_amount_a.unique_saturated_into();
let expected_amount: u64 = expected_amount_u128.try_into().ok()?;
match self {
Self::Legacy { outputs, .. } => {
let output = outputs.get(o_index)?;
if expected_amount != output.value {
return None;
}
if output.script_pubkey.len() != 57 {
return None;
}
let slice = &output.script_pubkey;
let receiver_address = self.parse_receiver(slice)?;
if receiver_address != destination {
return None;
}
let mut extra_data = [0u8; 32];
extra_data.copy_from_slice(&slice[25..57]);
Some(extra_data)
}
Self::SegWit {
witnesses, outputs, ..
} => {
let output = outputs.get(o_index)?;
if expected_amount != output.value {
return None;
}
let receiver_address = self.parse_receiver(&output.script_pubkey)?;
if receiver_address != destination {
return None;
}
let witness = witnesses.get(w_index)?;
let target_account: [u8; 32];
// NOTE: change it right after the real bridging tx
// will take place, or find something similar
#[cfg(any(feature = "runtime-benchmarks", feature = "std"))]
{
let _ = witness.stack;
target_account = [105u8; 32]; // 0x69 in decimal
}
#[cfg(not(any(feature = "runtime-benchmarks", feature = "std")))]
{
if witness.stack.len() < 3 || witness.stack[2].len() != 32 {
return None;
}
target_account = witness.stack[2][..32].try_into().ok()?;
}
Some(target_account)
}
}
}
fn parse_receiver(&self, script: &[u8]) -> Option<[u8; 32]> {
let mut receiver = [0u8; 32];
// 1. Parse Pay-to-Taproot (P2TR / SegWit v1)
// Markers: OP_1 (0x51) + OP_PUSHBYTES_32 (0x20) + 32 bytes key
if script.len() == 34 && script[0] == 0x51 && script[1] == 0x20 {
receiver.copy_from_slice(&script[2..34]);
return Some(receiver);
}
// 2. Parse Pay-to-Witness-Script-Hash (P2WSH / SegWit v0)
// Markers: OP_0 (0x00) + OP_PUSHBYTES_32 (0x20) + 32 bytes hash script
if script.len() == 34 && script[0] == 0x00 && script[1] == 0x20 {
receiver.copy_from_slice(&script[2..34]);
return Some(receiver);
}
// 3a. Parse Native SegWit 20-bytes key (P2WPKH) zero-padding
// Markers: 22 bytes len, starts with OP_0 (0x00), next OP_PUSHBYTES_20 (0x14)
if script.len() == 22 && script[0] == 0x00 && script[1] == 0x14 {
receiver[..20].copy_from_slice(&script[2..22]);
return Some(receiver);
}
// 3b. Parse Pay-to-Script-Hash (P2SH)
// Script: OP_HASH160 (0xa9) OP_PUSHBYTES_20 (0x14) + 20 bytes hash + OP_EQUAL (0x87)
if script.len() == 23 && script[0] == 0xa9 && script[1] == 0x14 && script[22] == 0x87 {
receiver[..20].copy_from_slice(&script[2..22]);
return Some(receiver);
}
// 4. Parse Legacy (P2PKH) zero-padding
// Script: OP_DUP(0x76) OP_HASH160(0xa9) OP_PUSHBYTES_20(0x14) +
// 20 bytes hash + OP_EQUALVERIFY(0x88) OP_CHECKSIG(0xac)
if script.len() == 25
&& script[0] == 0x76
&& script[1] == 0xa9
&& script[2] == 0x14
&& script[23] == 0x88
&& script[24] == 0xac
{
receiver[..20].copy_from_slice(&script[3..23]);
return Some(receiver);
}
// 5. Custom one: Legacy (P2PKH) + 32 bytes
// Script: [0..25] — basic P2PKH, [25..57] — 32 bytes of extra data
if script.len() == 57
&& script[0] == 0x76 // OP_DUP
&& script[1] == 0xa9 // OP_HASH160
&& script[2] == 0x14 // OP_PUSHBYTES_20
&& script[23] == 0x88 // OP_EQUALVERIFY
&& script[24] == 0xac
// OP_CHECKSIG
{
receiver[..20].copy_from_slice(&script[3..23]);
return Some(receiver);
}
None
}
}
#[derive(Eq, PartialEq)]
pub enum VarintError {
EmptyInput,
BufferTooShort,
TypeConversionFailed,
}
fn read_varint(bytes: &mut &[u8]) -> Result<usize, VarintError> {
let first = *bytes.get(0).ok_or(VarintError::EmptyInput)?;
*bytes = &bytes[1..];
match first {
0xfd => {
if bytes.len() < 2 {
return Err(VarintError::BufferTooShort);
}
let val = u16::from_le_bytes(
bytes[..2]
.try_into()
.map_err(|_| VarintError::TypeConversionFailed)?,
);
*bytes = &bytes[2..];
Ok(val as usize)
}
0xfe => {
if bytes.len() < 4 {
return Err(VarintError::BufferTooShort);
}
let val = u32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| VarintError::TypeConversionFailed)?,
);
*bytes = &bytes[4..];
Ok(val as usize)
}
0xff => {
if bytes.len() < 8 {
return Err(VarintError::BufferTooShort);
}
let val = u64::from_le_bytes(
bytes[..8]
.try_into()
.map_err(|_| VarintError::TypeConversionFailed)?,
);
*bytes = &bytes[8..];
Ok(val as usize)
}
_ => Ok(first as usize),
}
}
#[derive(Eq, PartialEq)]
pub enum ParseError {
InvalidLength,
ExceededMaxLimits,
TypeConversionFailed,
TransactionNotFoundInProof,
InvalidVarint(VarintError),
}
impl From<VarintError> for ParseError {
fn from(err: VarintError) -> Self {
ParseError::InvalidVarint(err)
}
}
impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Transaction<H> {
type Error = ParseError;
fn try_from(mut bytes: &[u8]) -> Result<Self, Self::Error> {
if bytes.len() < 4 {
return Err(ParseError::InvalidLength);
}
let version = i32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
bytes = &bytes[4..];
let is_segwit = bytes.len() >= 2 && bytes[0] == 0x00 && bytes[1] == 0x01;
if is_segwit {
bytes = &bytes[2..];
}
let input_count = read_varint(&mut bytes)?;
if input_count > MAX_INPUTS {
return Err(ParseError::ExceededMaxLimits);
}
let mut inputs_buffer: [Option<TxIn<H>>; MAX_INPUTS] = [const { None }; MAX_INPUTS];
for idx in 0..input_count {
if bytes.len() < 36 {
return Err(ParseError::InvalidLength);
}
let mut prev_txid = [0u8; 32];
prev_txid.copy_from_slice(&bytes[..32]);
let vout = u32::from_le_bytes(
bytes[32..36]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
bytes = &bytes[36..];
let script_len = read_varint(&mut bytes)?;
if script_len > MAX_SCRIPT_LEN {
return Err(ParseError::ExceededMaxLimits);
}
if bytes.len() < script_len {
return Err(ParseError::InvalidLength);
}
let script_sig = bytes[..script_len].to_vec();
bytes = &bytes[script_len..];
if bytes.len() < 4 {
return Err(ParseError::InvalidLength);
}
let sequence = u32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
bytes = &bytes[4..];
inputs_buffer[idx] = Some(TxIn {
prev_txid: H::from_slice(&prev_txid),
vout,
script_sig,
sequence,
});
}
let output_count = read_varint(&mut bytes)?;
if output_count > MAX_OUTPUTS {
return Err(ParseError::ExceededMaxLimits);
}
let mut outputs_buffer: [Option<TxOut>; MAX_OUTPUTS] = [const { None }; MAX_OUTPUTS];
for idx in 0..output_count {
if bytes.len() < 8 {
return Err(ParseError::InvalidLength);
}
let value = u64::from_le_bytes(
bytes[..8]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
bytes = &bytes[8..];
let script_len = read_varint(&mut bytes)?;
if script_len > MAX_SCRIPT_LEN {
return Err(ParseError::ExceededMaxLimits);
}
if bytes.len() < script_len {
return Err(ParseError::InvalidLength);
}
let script_pubkey = bytes[..script_len].to_vec();
bytes = &bytes[script_len..];
outputs_buffer[idx] = Some(TxOut {
value,
script_pubkey,
});
}
let inputs: Vec<TxIn<H>> = inputs_buffer[..input_count]
.iter_mut()
.filter_map(|opt| opt.take())
.collect();
let outputs: Vec<TxOut> = outputs_buffer[..output_count]
.iter_mut()
.filter_map(|opt| opt.take())
.collect();
if is_segwit {
let mut witnesses_buffer: [Option<Witness>; MAX_INPUTS] = [const { None }; MAX_INPUTS];
for idx in 0..input_count {
let item_count = read_varint(&mut bytes)?;
if item_count > MAX_WITNESS_STACK_DEPTH {
return Err(ParseError::ExceededMaxLimits);
}
let mut stack_buffer: [Option<Vec<u8>>; MAX_WITNESS_STACK_DEPTH] =
[const { None }; MAX_WITNESS_STACK_DEPTH];
for s_idx in 0..item_count {
let item_len = read_varint(&mut bytes)?;
if item_len > MAX_WITNESS_ITEMS_LEN {
return Err(ParseError::ExceededMaxLimits);
}
if bytes.len() < item_len {
return Err(ParseError::InvalidLength);
}
stack_buffer[s_idx] = Some(bytes[..item_len].to_vec());
bytes = &bytes[item_len..];
}
let stack: Vec<Vec<u8>> = stack_buffer[..item_count]
.iter_mut()
.filter_map(|opt| opt.take())
.collect();
witnesses_buffer[idx] = Some(Witness { stack });
}
let witnesses: Vec<Witness> = witnesses_buffer[..input_count]
.iter_mut()
.filter_map(|opt| opt.take())
.collect();
if bytes.len() < 4 {
return Err(ParseError::InvalidLength);
}
let lock_time = u32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
Ok(Transaction::SegWit {
version,
inputs,
outputs,
witnesses,
lock_time,
})
} else {
if bytes.len() < 4 {
return Err(ParseError::InvalidLength);
}
let lock_time = u32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
Ok(Transaction::Legacy {
version,
inputs,
outputs,
lock_time,
})
}
}
}
impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Header<H> {
type Error = ParseError;
fn try_from(mut bytes: &[u8]) -> Result<Self, Self::Error> {
if bytes.len() < 85 {
return Err(ParseError::InvalidLength);
}
let header_bytes = &bytes[..80];
bytes = &bytes[80..];
let mut merkle_root = [0u8; 32];
merkle_root.copy_from_slice(&header_bytes[36..68]);
if bytes.len() < 4 {
return Err(ParseError::InvalidLength);
}
let tx_count = u32::from_le_bytes(
bytes[..4]
.try_into()
.map_err(|_| ParseError::TypeConversionFailed)?,
);
bytes = &bytes[4..];
let hash_count = read_varint(&mut bytes)?;
if bytes.len() < hash_count * 32 {
return Err(ParseError::InvalidLength);
}
let tree_hashes_bytes = &bytes[..hash_count * 32];
bytes = &bytes[hash_count * 32..];
let tree_hashes: Vec<H::Hash> = tree_hashes_bytes
.chunks_exact(32)
.map(|chunk| H::from_slice(chunk))
.collect();
let flags_count = read_varint(&mut bytes)?;
if bytes.len() < flags_count {
return Err(ParseError::InvalidLength);
}
let flags = bytes[..flags_count].to_vec();
bytes = &bytes[flags_count..];
if !bytes.is_empty() {
return Err(ParseError::InvalidLength);
}
Ok(Self {
merkle_root: H::from_slice(&merkle_root),
tx_count,
tree_hashes,
flags,
})
}
}

View File

@ -1,7 +1,6 @@
[package]
name = "ghost-networks"
version = "0.2.7"
description = "Registry and lifecycle management for external network metadata and cryptographic profiles."
version = "0.1.26"
license.workspace = true
authors.workspace = true
edition.workspace = true
@ -11,7 +10,6 @@ repository.workspace = true
[dependencies]
scale-info = { workspace = true, features = ["derive"] }
codec = { workspace = true, features = ["max-encoded-len"] }
hex = { workspace = true, features = ["alloc"] }
num-traits = { workspace = true }
log = { workspace = true }
@ -22,7 +20,6 @@ pallet-staking = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
ghost-traits = { workspace = true }
ghost-helpers = { workspace = true }
[dev-dependencies]
primitives = { workspace = true }
@ -44,7 +41,6 @@ std = [
"sp-std/std",
"sp-io/std",
"ghost-traits/std",
"ghost-helpers/std",
"pallet-staking/std",
"pallet-balances/std",
]

View File

@ -6,10 +6,9 @@ use crate::Pallet as GhostNetworks;
use frame_benchmarking::v1::{benchmarks, BenchmarkError};
use sp_runtime::Saturating;
use ghost_helpers::networks::{
MAX_ENDPOINT_LEN, MAX_ENDPOINTS_LEN, MAX_GATEKEEPER_LEN,
MAX_SELECTOR_LEN,
};
const MAX_NAME_LEN: u32 = 20;
const MAX_ENDPOINT_LEN: u32 = 150;
const MAX_ENDPOINT_NUMBER: u32 = 20;
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
@ -19,13 +18,18 @@ fn prepare_network<T: Config>(
n: u32,
m: u32,
k: u32,
) -> (T::NetworkId, NetworkData) {
) -> (<T as module::Config>::NetworkId, NetworkData) {
let chain_id: <T as module::Config>::NetworkId = Default::default();
let chain_id = chain_id.saturating_add((n + m).into());
let mut gatekeeper = sp_std::vec![];
for i in 0..n {
gatekeeper.push((i % 256) as u8);
let mut gatekeeper = b"0x".to_vec();
for i in 0..40 {
gatekeeper.push(i);
}
let mut topic_name = b"0x".to_vec();
for i in 0..64 {
topic_name.push(i);
}
let mut default_endpoints = sp_std::vec![];
@ -33,24 +37,25 @@ fn prepare_network<T: Config>(
default_endpoints.push(sp_std::vec![0x69; m as usize]);
}
let network = ghost_helpers::networks::NetworkDataBuilder::default()
.with_gatekeeper(gatekeeper)
.with_default_endpoints(default_endpoints)
.with_selector(vec![1u8; 4])
.with_network_type(NetworkType::Evm)
.with_network_curve(NetworkCurve::Secp256k1)
.with_rate_limit_delay(6)
.with_finality_delay(69)
.with_block_deviation(420)
.with_incoming_fee(0)
.with_outgoing_fee(0)
.build();
let network = NetworkData {
chain_name: sp_std::vec![0x69; n as usize],
default_endpoints,
gatekeeper,
topic_name,
network_type: NetworkType::Evm,
avg_block_speed: 12,
finality_delay: 69,
rate_limit_delay: 69,
block_distance: 69,
incoming_fee: 0,
outgoing_fee: 0,
};
(chain_id, network)
}
fn create_network<T: Config>(
chain_id: T::NetworkId,
chain_id: <T as module::Config>::NetworkId,
network: NetworkData,
) -> Result<Option<NetworkData>, BenchmarkError> {
let prev_network = match GhostNetworks::<T>::get(&chain_id) {
@ -59,8 +64,8 @@ fn create_network<T: Config>(
let authority = T::RegisterOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
GhostNetworks::<T>::register_network(
authority,
chain_id,
authority.clone(),
chain_id.clone(),
network.clone(),
)
.map_err(|_| BenchmarkError::Weightless)?;
@ -73,52 +78,55 @@ fn create_network<T: Config>(
benchmarks! {
register_network {
let i in 1 .. MAX_GATEKEEPER_LEN;
let i in 1 .. MAX_NAME_LEN;
let j in 1 .. MAX_ENDPOINT_LEN;
let k in 1 .. MAX_ENDPOINTS_LEN;
let k in 1 .. MAX_ENDPOINT_NUMBER;
let (chain_id, network) = prepare_network::<T>(i, j, k);
let authority = T::RegisterOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
assert_eq!(GhostNetworks::<T>::networks(chain_id), None);
assert_eq!(GhostNetworks::<T>::networks(chain_id.clone()), None);
assert!(GhostNetworks::<T>::network_indexes().is_empty());
}: _<T::RuntimeOrigin>(authority, chain_id, network.clone())
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), network.clone())
verify {
assert_last_event::<T>(Event::NetworkRegistered {chain_id: chain_id}.into());
assert_eq!(GhostNetworks::<T>::networks(chain_id), Some(network));
assert_last_event::<T>(Event::NetworkRegistered {
chain_id: chain_id.clone(), network: network.clone(),
}.into());
assert_eq!(GhostNetworks::<T>::networks(chain_id.clone()), Some(network));
assert_eq!(GhostNetworks::<T>::network_indexes(), vec![chain_id]);
}
update_network_selector {
let raw_selector = sp_std::vec![0x42; MAX_SELECTOR_LEN as usize];
let selector = BoundedVec::<u8, ConstU32<MAX_SELECTOR_LEN>>::try_from(raw_selector)
.map_err(|_| BenchmarkError::Weightless)?;
update_network_name {
let n in 1 .. MAX_NAME_LEN;
let name = sp_std::vec![0x42; n as usize];
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, selector)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), name.clone())
verify {
assert_last_event::<T>(Event::NetworkSelectorUpdated { chain_id }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkNameUpdated {
chain_id: chain_id.clone(), chain_name: name,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_endpoint {
let n in 1 .. MAX_ENDPOINT_LEN;
let index = 0u32;
let raw_endpoint = sp_std::vec![0x42; n as usize];
let endpoint = BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(raw_endpoint)
.map_err(|_| BenchmarkError::Weightless)?;
let index_to_update = 0u32;
let endpoint = sp_std::vec![0x42; n as usize];
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, Some(index), Some(endpoint))
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), Some(index_to_update), Some(endpoint.clone()))
verify {
assert_last_event::<T>(Event::NetworkEndpointUpdated {chain_id, index }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkEndpointUpdated {
chain_id: chain_id.clone(),
index: index_to_update,
endpoint,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_finality_delay {
@ -126,13 +134,13 @@ benchmarks! {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, delay)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), delay)
verify {
assert_last_event::<T>(Event::NetworkFinalityDelayUpdated {
chain_id, finality_delay: delay,
chain_id: chain_id.clone(), finality_delay: delay,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_rate_limit_delay {
@ -140,25 +148,27 @@ benchmarks! {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, rate_limit)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), rate_limit)
verify {
assert_last_event::<T>(Event::NetworkRateLimitDelayUpdated {
chain_id, rate_limit_delay: rate_limit,
chain_id: chain_id.clone(), rate_limit_delay: rate_limit,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_block_deviation {
let block_deviation = 1337;
update_network_block_distance {
let block_distance = 1337;
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, block_deviation)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), block_distance)
verify {
assert_last_event::<T>(Event::NetworkBlockDeviationUpdated { chain_id, block_deviation }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkBlockDistanceUpdated {
chain_id: chain_id.clone(), block_distance,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_type {
@ -166,42 +176,42 @@ benchmarks! {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, network_type.clone())
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), network_type.clone())
verify {
assert_last_event::<T>(Event::NetworkTypeUpdated { chain_id, network_type }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
}
update_network_curve {
let network_curve = NetworkCurve::Ed25519;
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, network_curve)
verify {
assert_last_event::<T>(Event::NetworkCurveUpdated { chain_id, network_curve }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkTypeUpdated {
chain_id: chain_id.clone(), network_type: network_type.clone(),
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_gatekeeper {
let mut raw_gatekeeper = b"0x".to_vec();
for i in 0..40 { raw_gatekeeper.push(i + 1); }
let gatekeeper =
BoundedVec::<u8, ConstU32<MAX_GATEKEEPER_LEN>>::try_from(raw_gatekeeper)
.map_err(|_| BenchmarkError::Weightless)?;
let mut gatekeeper = b"0x".to_vec();
for i in 0..40 { gatekeeper.push(i + 1); }
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, gatekeeper)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), gatekeeper.clone())
verify {
assert_last_event::<T>(Event::NetworkGatekeeperUpdated { chain_id }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkGatekeeperUpdated {
chain_id: chain_id.clone(), gatekeeper,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_network_topic_name {
let topic_name = b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec();
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), topic_name.clone())
verify {
assert_last_event::<T>(Event::NetworkTopicNameUpdated {
chain_id: chain_id.clone(), topic_name,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_incoming_network_fee {
@ -209,10 +219,12 @@ benchmarks! {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, incoming_fee)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), incoming_fee)
verify {
assert_last_event::<T>(Event::NetworkIncomingFeeUpdated { chain_id, incoming_fee }.into());
assert_last_event::<T>(Event::NetworkIncomingFeeUpdated {
chain_id: chain_id.clone(), incoming_fee,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
@ -221,24 +233,42 @@ benchmarks! {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id, network)?;
}: _<T::RuntimeOrigin>(authority, chain_id, outgoing_fee)
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), outgoing_fee)
verify {
assert_last_event::<T>(Event::NetworkOutgoingFeeUpdated { chain_id, outgoing_fee }.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id), prev_network);
assert_last_event::<T>(Event::NetworkOutgoingFeeUpdated {
chain_id: chain_id.clone(), outgoing_fee,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), prev_network);
}
update_avg_block_speed {
let avg_block_speed = 420;
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::UpdateOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let prev_network = create_network::<T>(chain_id.clone(), network.clone())?;
}: _<T::RuntimeOrigin>(authority, chain_id.clone(), avg_block_speed)
verify {
assert_last_event::<T>(Event::NetworkAvgBlockSpeedUpdated {
chain_id: chain_id.clone(), avg_block_speed,
}.into());
assert_ne!(GhostNetworks::<T>::networks(chain_id.clone()), None);
}
remove_network {
let (chain_id, network) = prepare_network::<T>(1, 1, 1);
let authority = T::RemoveOrigin::try_successful_origin()
.map_err(|_| BenchmarkError::Weightless)?;
let maybe_network = create_network::<T>(chain_id, network)?;
assert_eq!(GhostNetworks::<T>::networks(chain_id), maybe_network);
assert_eq!(GhostNetworks::<T>::network_indexes(), vec![chain_id]);
}: _<T::RuntimeOrigin>(authority, chain_id)
let _ = create_network::<T>(chain_id.clone(), network.clone())?;
assert_eq!(GhostNetworks::<T>::networks(chain_id.clone()), Some(network));
assert_eq!(GhostNetworks::<T>::network_indexes(), vec![chain_id.clone()]);
}: _<T::RuntimeOrigin>(authority, chain_id.clone())
verify {
assert_last_event::<T>(Event::NetworkRemoved { chain_id }.into());
assert_eq!(GhostNetworks::<T>::networks(chain_id), None);
assert_last_event::<T>(Event::NetworkRemoved {
chain_id: chain_id.clone(),
}.into());
assert_eq!(GhostNetworks::<T>::networks(chain_id.clone()), None);
assert!(GhostNetworks::<T>::network_indexes().is_empty());
}

View File

@ -17,15 +17,11 @@ use sp_runtime::{
};
use sp_std::{convert::TryInto, prelude::*};
use ghost_traits::networks::{
pub use ghost_traits::networks::{
NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler,
};
use ghost_helpers::networks::{
MAX_ENDPOINT_LEN, MAX_GATEKEEPER_LEN, MAX_SELECTOR_LEN,
NetworkData, NetworkType, NetworkCurve, NetworkInitiation,
};
pub mod migrations;
mod weights;
pub use crate::weights::WeightInfo;
@ -38,9 +34,39 @@ mod mock;
#[cfg(all(feature = "std", test))]
mod tests;
const LOG_TARGET: &str = "runtime::ghost-networks";
pub type BalanceOf<T> =
<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum NetworkType {
Evm = 0,
Utxo = 1,
Undefined = 2,
}
impl Default for NetworkType {
fn default() -> Self {
NetworkType::Evm
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct NetworkData {
pub chain_name: Vec<u8>,
pub default_endpoints: Vec<Vec<u8>>,
pub gatekeeper: Vec<u8>,
pub topic_name: Vec<u8>,
pub network_type: NetworkType,
pub finality_delay: u64,
pub rate_limit_delay: u64,
pub block_distance: u64,
pub avg_block_speed: u64,
pub incoming_fee: u32,
pub outgoing_fee: u32,
}
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct BridgeAdjustment<Balance> {
pub bridged_out: Balance,
@ -148,34 +174,71 @@ pub mod module {
WrongTopicName,
/// Could not store networks into bounded vector.
TooManyNetworks,
/// Selector length should always be 4.
InvalidSelectorLen,
/// Index is out of possible range.
IndexOutOfBounds,
/// No more default endpoints available to store.
TooManyEndpoints,
/// Network type does not exist under specified curve.
NetworkTypeDoesNotExist,
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
NetworkRegistered { chain_id: T::NetworkId },
NetworkSelectorUpdated { chain_id: T::NetworkId },
NetworkEndpointUpdated { index: u32, chain_id: T::NetworkId },
NetworkEndpointRemoved { index: u32, chain_id: T::NetworkId },
NetworkEndpointAdded { chain_id: T::NetworkId },
NetworkFinalityDelayUpdated { chain_id: T::NetworkId, finality_delay: u64 },
NetworkRateLimitDelayUpdated { chain_id: T::NetworkId, rate_limit_delay: u64 },
NetworkBlockDeviationUpdated { chain_id: T::NetworkId, block_deviation: u64 },
NetworkTypeUpdated { chain_id: T::NetworkId, network_type: NetworkType },
NetworkCurveUpdated { chain_id: T::NetworkId, network_curve: NetworkCurve },
NetworkGatekeeperUpdated { chain_id: T::NetworkId },
NetworkIncomingFeeUpdated { chain_id: T::NetworkId, incoming_fee: u32 },
NetworkOutgoingFeeUpdated { chain_id: T::NetworkId, outgoing_fee: u32 },
NetworkAvgBlockSpeedUpdated { chain_id: T::NetworkId, avg_block_speed: u64 },
NetworkRemoved { chain_id: T::NetworkId },
NetworkRegistered {
chain_id: T::NetworkId,
network: NetworkData,
},
NetworkNameUpdated {
chain_id: T::NetworkId,
chain_name: Vec<u8>,
},
NetworkEndpointUpdated {
chain_id: T::NetworkId,
index: u32,
endpoint: Vec<u8>,
},
NetworkEndpointRemoved {
chain_id: T::NetworkId,
index: u32,
},
NetworkEndpointAdded {
chain_id: T::NetworkId,
endpoint: Vec<u8>,
},
NetworkFinalityDelayUpdated {
chain_id: T::NetworkId,
finality_delay: u64,
},
NetworkRateLimitDelayUpdated {
chain_id: T::NetworkId,
rate_limit_delay: u64,
},
NetworkBlockDistanceUpdated {
chain_id: T::NetworkId,
block_distance: u64,
},
NetworkTypeUpdated {
chain_id: T::NetworkId,
network_type: NetworkType,
},
NetworkGatekeeperUpdated {
chain_id: T::NetworkId,
gatekeeper: Vec<u8>,
},
NetworkTopicNameUpdated {
chain_id: T::NetworkId,
topic_name: Vec<u8>,
},
NetworkIncomingFeeUpdated {
chain_id: T::NetworkId,
incoming_fee: u32,
},
NetworkOutgoingFeeUpdated {
chain_id: T::NetworkId,
outgoing_fee: u32,
},
NetworkAvgBlockSpeedUpdated {
chain_id: T::NetworkId,
avg_block_speed: u64,
},
NetworkRemoved {
chain_id: T::NetworkId,
},
}
#[pallet::storage]
@ -192,28 +255,10 @@ pub mod module {
pub type NetworkIndexes<T: Config> =
StorageValue<_, BoundedVec<T::NetworkId, T::MaxNetworks>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn network_curves)]
pub type NetworkCurves<T: Config> = StorageMap<
_,
Twox64Concat, NetworkCurve,
BoundedBTreeMap<NetworkType, u32, T::MaxNetworks>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn network_curves_vec)]
pub type NetworkCurvesVec<T: Config> =
StorageValue<_, BoundedVec<NetworkCurve, T::MaxNetworks>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn networks)]
pub type Networks<T: Config> = StorageMap<
_,
Twox64Concat, T::NetworkId,
NetworkData,
OptionQuery,
>;
pub type Networks<T: Config> =
StorageMap<_, Twox64Concat, T::NetworkId, NetworkData, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn gatekeeper_amount)]
@ -222,26 +267,29 @@ pub mod module {
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub networks: Vec<NetworkInitiation<T::NetworkId, BalanceOf<T>>>,
pub networks: Vec<(T::NetworkId, Vec<u8>, BalanceOf<T>)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self { networks: sp_std::vec![] }
Self { networks: vec![] }
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
if !self.networks.is_empty() {
self.networks
.iter()
.for_each(|network| {
Pallet::<T>::do_register_network(network.id, network.data.clone())
.for_each(|(chain_id, network_metadata, gatekeeper_amount)| {
let network = NetworkData::decode(&mut &network_metadata[..])
.expect("Error decoding NetworkData");
Pallet::<T>::do_register_network(chain_id.clone(), network)
.expect("Error registering network");
GatekeeperAmount::<T>::insert(network.id, network.amount);
GatekeeperAmount::<T>::insert(chain_id, gatekeeper_amount);
});
}
}
}
@ -254,13 +302,13 @@ pub mod module {
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::register_network(
network.gatekeeper.len() as u32,
network.selector.len() as u32,
network.chain_name.len() as u32,
network.default_endpoints
.iter()
.map(|endpoint| endpoint.len())
.max()
.unwrap_or_default() as u32,
network.default_endpoints.len() as u32,
))]
pub fn register_network(
origin: OriginFor<T>,
@ -272,14 +320,16 @@ pub mod module {
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::update_network_selector())]
pub fn update_network_selector(
#[pallet::weight(T::WeightInfo::update_network_name(
chain_name.len() as u32,
))]
pub fn update_network_name(
origin: OriginFor<T>,
chain_id: T::NetworkId,
selector: BoundedVec<u8, ConstU32<MAX_SELECTOR_LEN>>,
chain_name: Vec<u8>,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_nework_selector(chain_id, selector)
Self::do_update_network_name(chain_id, chain_name)
}
#[pallet::call_index(2)]
@ -293,7 +343,7 @@ pub mod module {
origin: OriginFor<T>,
chain_id: T::NetworkId,
maybe_index: Option<u32>,
maybe_endpoint: Option<BoundedVec<u8, ConstU32<MAX_ENDPOINT_LEN>>>,
maybe_endpoint: Option<Vec<u8>>,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_network_endpoint(chain_id, maybe_index, maybe_endpoint)
@ -322,14 +372,14 @@ pub mod module {
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::update_network_block_deviation())]
pub fn update_network_block_deviation(
#[pallet::weight(T::WeightInfo::update_network_block_distance())]
pub fn update_network_block_distance(
origin: OriginFor<T>,
chain_id: T::NetworkId,
block_deviation: u64,
block_distance: u64,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_network_block_deviation(chain_id, block_deviation)
Self::do_update_network_block_distance(chain_id, block_distance)
}
#[pallet::call_index(6)]
@ -344,27 +394,27 @@ pub mod module {
}
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::update_network_curve())]
pub fn update_network_curve(
origin: OriginFor<T>,
chain_id: T::NetworkId,
network_curve: NetworkCurve,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_network_curve(chain_id, network_curve)
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::update_network_gatekeeper())]
pub fn update_network_gatekeeper(
origin: OriginFor<T>,
chain_id: T::NetworkId,
gatekeeper: BoundedVec<u8, ConstU32<MAX_GATEKEEPER_LEN>>,
gatekeeper: Vec<u8>,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_network_gatekeeper(chain_id, gatekeeper)
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::update_network_topic_name())]
pub fn update_network_topic_name(
origin: OriginFor<T>,
chain_id: T::NetworkId,
topic_name: Vec<u8>,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_network_topic_name(chain_id, topic_name)
}
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::update_incoming_network_fee())]
pub fn update_incoming_network_fee(
@ -388,6 +438,17 @@ pub mod module {
}
#[pallet::call_index(11)]
#[pallet::weight(T::WeightInfo::update_avg_block_speed())]
pub fn update_avg_block_speed(
origin: OriginFor<T>,
chain_id: T::NetworkId,
avg_block_speed: u64,
) -> DispatchResult {
T::UpdateOrigin::ensure_origin_or_root(origin)?;
Self::do_update_avg_block_speed(chain_id, avg_block_speed)
}
#[pallet::call_index(12)]
#[pallet::weight(T::WeightInfo::remove_network())]
pub fn remove_network(origin: OriginFor<T>, chain_id: T::NetworkId) -> DispatchResult {
T::RemoveOrigin::ensure_origin_or_root(origin)?;
@ -398,105 +459,53 @@ pub mod module {
impl<T: Config> Pallet<T> {
/// Register a new network.
pub fn do_register_network(
chain_id: T::NetworkId,
network: NetworkData,
) -> DispatchResult {
let network_curve = network.curve;
let network_type = network.r#type;
pub fn do_register_network(chain_id: T::NetworkId, network: NetworkData) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
ensure!(
!Networks::<T>::contains_key(&chain_id),
Error::<T>::NetworkAlreadyRegistered,
maybe_network.is_none(),
Error::<T>::NetworkAlreadyRegistered
);
*maybe_network = Some(network.clone());
Ok(())
})?;
let mut modified_indexes = NetworkIndexes::<T>::get();
modified_indexes.try_push(chain_id.clone())
NetworkIndexes::<T>::try_mutate(|ids| -> DispatchResult {
ids.try_push(chain_id)
.map_err(|_| Error::<T>::TooManyNetworks)?;
Ok(())
})?;
let mut modified_curves_map = NetworkCurves::<T>::get(network_curve);
if let Some(counter) = modified_curves_map.get_mut(&network_type) {
*counter = counter.saturating_add(1);
} else {
modified_curves_map.try_insert(network_type, 1u32)
.map_err(|_| Error::<T>::TooManyNetworks)?;
}
let mut modified_curves_vec = NetworkCurvesVec::<T>::get();
if !modified_curves_vec.contains(&network_curve) {
modified_curves_vec.try_push(network_curve)
.map_err(|_| Error::<T>::TooManyNetworks)?;
}
Networks::<T>::insert(&chain_id, network);
NetworkIndexes::<T>::put(modified_indexes);
NetworkCurves::<T>::insert(network_curve, modified_curves_map);
NetworkCurvesVec::<T>::put(modified_curves_vec);
Self::deposit_event(Event::<T>::NetworkRegistered { chain_id });
Self::deposit_event(Event::<T>::NetworkRegistered { chain_id, network });
Ok(())
}
/// Remove existent network.
pub fn do_remove_network(chain_id: T::NetworkId) -> DispatchResult {
let network = Networks::<T>::get(&chain_id)
.ok_or(Error::<T>::NetworkDoesNotExist)?;
let network_curve = network.curve;
let network_type = network.r#type;
let mut modified_curves_map = NetworkCurves::<T>::get(network_curve);
let counter = modified_curves_map
.get_mut(&network_type)
.ok_or(Error::<T>::NetworkTypeDoesNotExist)?;
*counter = counter.saturating_sub(1);
let mut should_remove_curve_from_vec = false;
if *counter == 0 {
modified_curves_map.remove(&network_type);
if modified_curves_map.is_empty() {
should_remove_curve_from_vec = true;
}
}
let mut modified_indexes = NetworkIndexes::<T>::get();
modified_indexes.retain(|id| id != &chain_id);
let mut modified_curves_vec = NetworkCurvesVec::<T>::get();
if should_remove_curve_from_vec {
modified_curves_vec.retain(|curve| curve != &network_curve);
}
Networks::<T>::remove(&chain_id);
NetworkIndexes::<T>::put(modified_indexes);
NetworkCurvesVec::<T>::put(modified_curves_vec);
if modified_curves_map.is_empty() {
NetworkCurves::<T>::remove(network_curve);
} else {
NetworkCurves::<T>::insert(network_curve, modified_curves_map);
}
NetworkIndexes::<T>::mutate(|ids| ids.retain(|id| id != &chain_id));
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
*maybe_network = None;
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkRemoved { chain_id });
Ok(())
}
/// Update existent network name.
pub fn do_update_nework_selector(
chain_id: T::NetworkId,
selector: BoundedVec<u8, ConstU32<MAX_SELECTOR_LEN>>,
) -> DispatchResult {
pub fn do_update_network_name(chain_id: T::NetworkId, chain_name: Vec<u8>) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
ensure!(selector.len() == 4, Error::<T>::InvalidSelectorLen);
let network = maybe_network.as_mut().ok_or(Error::<T>::NetworkDoesNotExist)?;
network.selector = selector;
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.chain_name = chain_name.clone();
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkSelectorUpdated { chain_id });
Self::deposit_event(Event::<T>::NetworkNameUpdated {
chain_id,
chain_name,
});
Ok(())
}
@ -504,31 +513,38 @@ impl<T: Config> Pallet<T> {
pub fn do_update_network_endpoint(
chain_id: T::NetworkId,
maybe_index: Option<u32>,
maybe_endpoint: Option<BoundedVec<u8, ConstU32<MAX_ENDPOINT_LEN>>>,
maybe_endpoint: Option<Vec<u8>>,
) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
let network = maybe_network.as_mut().ok_or(Error::<T>::NetworkDoesNotExist)?;
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let updated_network = maybe_network.as_mut().unwrap();
match (maybe_index, maybe_endpoint) {
(Some(index), Some(endpoint)) => {
let idx = index as usize;
ensure!(idx < network.default_endpoints.len(), Error::<T>::IndexOutOfBounds);
let _ = core::mem::replace(&mut network.default_endpoints[idx], endpoint);
Self::deposit_event(Event::<T>::NetworkEndpointUpdated { chain_id, index });
if let Some(previous_endpoint) =
updated_network.default_endpoints.get_mut(index as usize)
{
*previous_endpoint = endpoint.clone();
Self::deposit_event(Event::<T>::NetworkEndpointUpdated {
chain_id,
index,
endpoint,
});
}
}
(None, Some(endpoint)) => {
network.default_endpoints.try_push(endpoint).map_err(|_| Error::<T>::TooManyEndpoints)?;
Self::deposit_event(Event::<T>::NetworkEndpointAdded { chain_id });
updated_network.default_endpoints.push(endpoint.clone());
Self::deposit_event(Event::<T>::NetworkEndpointAdded { chain_id, endpoint });
}
(Some(index), None) => {
let idx = index as usize;
ensure!(idx < network.default_endpoints.len(), Error::<T>::IndexOutOfBounds);
network.default_endpoints.remove(idx);
updated_network.default_endpoints.remove(index as usize);
Self::deposit_event(Event::<T>::NetworkEndpointRemoved { chain_id, index });
}
(None, None) => {}
}
*maybe_network = Some(updated_network.clone());
Ok(())
})?;
Ok(())
})
}
/// Update existent network default finality delay.
@ -569,22 +585,22 @@ impl<T: Config> Pallet<T> {
Ok(())
}
/// Update existent network default block deviation between blocks.
pub fn do_update_network_block_deviation(
/// Update existent network default max distance between blocks.
pub fn do_update_network_block_distance(
chain_id: T::NetworkId,
block_deviation: u64,
block_distance: u64,
) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
let network = maybe_network.as_mut().ok_or(Error::<T>::NetworkDoesNotExist)?;
network.block_deviation = block_deviation;
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.block_distance = block_distance;
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkBlockDeviationUpdated {
Self::deposit_event(Event::<T>::NetworkBlockDistanceUpdated {
chain_id,
block_deviation,
block_distance,
});
Ok(())
}
@ -594,8 +610,10 @@ impl<T: Config> Pallet<T> {
network_type: NetworkType,
) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
let network = maybe_network.as_mut().ok_or(Error::<T>::NetworkDoesNotExist)?;
network.r#type = network_type;
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.network_type = network_type;
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkTypeUpdated {
@ -605,86 +623,49 @@ impl<T: Config> Pallet<T> {
Ok(())
}
/// Update underlying network curve.
pub fn do_update_network_curve(
chain_id: T::NetworkId,
network_curve: NetworkCurve,
) -> DispatchResult {
let mut network = Networks::<T>::get(&chain_id)
.ok_or(Error::<T>::NetworkDoesNotExist)?;
let prev_curve = network.curve;
let network_type = network.r#type;
if prev_curve == network_curve {
return Ok(());
}
let mut modified_prev_map = NetworkCurves::<T>::get(prev_curve);
let counter = modified_prev_map
.get_mut(&network_type)
.ok_or(Error::<T>::NetworkTypeDoesNotExist)?;
*counter = counter.saturating_sub(1);
let mut should_remove_prev_from_vec = false;
if *counter == 0 {
modified_prev_map.remove(&network_type);
if modified_prev_map.is_empty() {
should_remove_prev_from_vec = true;
}
}
let mut modified_curr_map = NetworkCurves::<T>::get(network_curve);
if let Some(counter) = modified_curr_map.get_mut(&network_type) {
*counter = counter.saturating_add(1);
} else {
modified_curr_map.try_insert(network_type, 1u32)
.map_err(|_| Error::<T>::TooManyNetworks)?;
}
let mut modified_curves_vec = NetworkCurvesVec::<T>::get();
if should_remove_prev_from_vec {
modified_curves_vec.retain(|curve| curve != &prev_curve);
}
if !modified_curves_vec.contains(&network_curve) {
modified_curves_vec.try_push(network_curve)
.map_err(|_| Error::<T>::TooManyNetworks)?;
}
network.curve = network_curve;
Networks::<T>::insert(&chain_id, network);
NetworkCurves::<T>::insert(network_curve, modified_curr_map);
NetworkCurvesVec::<T>::put(modified_curves_vec);
if modified_prev_map.is_empty() {
NetworkCurves::<T>::remove(prev_curve);
} else {
NetworkCurves::<T>::insert(prev_curve, modified_prev_map);
}
Self::deposit_event(Event::<T>::NetworkCurveUpdated {
chain_id,
network_curve,
});
Ok(())
}
/// Update existent network gatekeeper.
pub fn do_update_network_gatekeeper(
chain_id: T::NetworkId,
gatekeeper: BoundedVec<u8, ConstU32<MAX_GATEKEEPER_LEN>>,
gatekeeper: Vec<u8>,
) -> DispatchResult {
ensure!(
gatekeeper.len() == 42 && gatekeeper[0] == 48 && gatekeeper[1] == 120,
Error::<T>::WrongGatekeeperAddress
);
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
let network = maybe_network.as_mut().ok_or(Error::<T>::NetworkDoesNotExist)?;
network.gatekeeper = gatekeeper;
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.gatekeeper = gatekeeper.clone();
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkGatekeeperUpdated { chain_id });
Self::deposit_event(Event::<T>::NetworkGatekeeperUpdated {
chain_id,
gatekeeper,
});
Ok(())
}
/// Update existent network gatekeeper's topic name.
pub fn do_update_network_topic_name(
chain_id: T::NetworkId,
topic_name: Vec<u8>,
) -> DispatchResult {
ensure!(
topic_name.len() == 66 && topic_name[0] == 48 && topic_name[1] == 120,
Error::<T>::WrongTopicName
);
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.topic_name = topic_name.clone();
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkTopicNameUpdated {
chain_id,
topic_name,
});
Ok(())
}
@ -723,12 +704,28 @@ impl<T: Config> Pallet<T> {
});
Ok(())
}
pub fn do_update_avg_block_speed(
chain_id: T::NetworkId,
avg_block_speed: u64,
) -> DispatchResult {
Networks::<T>::try_mutate(&chain_id, |maybe_network| -> DispatchResult {
ensure!(maybe_network.is_some(), Error::<T>::NetworkDoesNotExist);
let net = maybe_network.as_mut().unwrap();
net.avg_block_speed = avg_block_speed;
*maybe_network = Some(net.clone());
Ok(())
})?;
Self::deposit_event(Event::<T>::NetworkAvgBlockSpeedUpdated {
chain_id,
avg_block_speed,
});
Ok(())
}
}
impl<T: Config> NetworkDataBasicHandler for Pallet<T> {
type NetworkId = T::NetworkId;
type NetworkType = NetworkType;
type NetworkCurve = NetworkCurve;
}
impl<T: Config> NetworkDataInspectHandler<NetworkData> for Pallet<T> {
@ -742,10 +739,6 @@ impl<T: Config> NetworkDataInspectHandler<NetworkData> for Pallet<T> {
Networks::<T>::contains_key(n)
}
fn curve_exists(curve: &Self::NetworkCurve) -> bool {
NetworkCurves::<T>::contains_key(curve)
}
fn get(n: &Self::NetworkId) -> Option<NetworkData> {
Networks::<T>::get(n)
}
@ -770,18 +763,6 @@ impl<T: Config> NetworkDataInspectHandler<NetworkData> for Pallet<T> {
fn iter_indexes() -> impl Iterator<Item = Self::NetworkId> {
NetworkIndexes::<T>::get().into_iter()
}
fn iter_curves() -> impl Iterator<Item = Self::NetworkCurve> {
NetworkCurvesVec::<T>::get().into_iter()
}
fn iter_types_by_curve(
curve: &Self::NetworkCurve
) -> impl Iterator<Item = Self::NetworkType> {
NetworkCurves::<T>::get(curve)
.into_iter()
.map(|(network_type, _)| network_type)
}
}
impl<T: Config> NetworkDataMutateHandler<NetworkData, BalanceOf<T>> for Pallet<T> {

View File

@ -0,0 +1,9 @@
pub mod v1;
pub type MigrateV0ToV1<T> = frame_support::migrations::VersionedMigration<
0,
1,
v1::StoreNetworkIdsIntoBoundedVec<T>,
crate::Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;

View File

@ -0,0 +1,59 @@
use frame_support::{
migration::clear_storage_prefix,
traits::{Get, PalletInfoAccess, UncheckedOnRuntimeUpgrade},
weights::Weight,
};
use sp_std::marker::PhantomData;
use crate::{BoundedVec, Config, NetworkIndexes, Networks, Pallet, Vec, LOG_TARGET};
pub struct StoreNetworkIdsIntoBoundedVec<T>(PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for StoreNetworkIdsIntoBoundedVec<T> {
fn on_runtime_upgrade() -> Weight {
let mut weight = T::DbWeight::get().reads(1);
let network_ids: Vec<T::NetworkId> = Networks::<T>::iter_keys().collect();
let networks_count = network_ids.len();
weight = weight.saturating_add(T::DbWeight::get().reads(networks_count as u64));
let clear_results = clear_storage_prefix(
Pallet::<T>::name().as_bytes(),
"NullifyNeeded".as_bytes(),
&[],
Some(1),
None,
);
log::info!(
target: LOG_TARGET,
"⛓️ NullifyNeeded storage succesfully removed: Loops (reads): {}, Unique removed (writes): {}",
clear_results.loops,
clear_results.unique,
);
weight = weight.saturating_add(T::DbWeight::get().reads(clear_results.loops as u64));
weight = weight.saturating_add(T::DbWeight::get().writes(clear_results.unique as u64));
let writes = BoundedVec::<T::NetworkId, T::MaxNetworks>::try_from(network_ids)
.inspect_err(|err| {
log::error!(
target: LOG_TARGET,
"⛓️ Network ids to bounded_vec migration failed: {:?}",
err,
)
})
.ok()
.map(|bounded_networks| {
NetworkIndexes::<T>::put(bounded_networks);
log::info!(
target: LOG_TARGET,
"⛓️ Network ids to bounded_vec migration success: {} networks moved",
networks_count,
);
1u64
})
.unwrap_or_default();
weight.saturating_add(T::DbWeight::get().writes(writes))
}
}

View File

@ -7,34 +7,30 @@ use mock::{
use pallet_staking::EraPayout;
use sp_runtime::DispatchError;
const CURRENT_CURVE: NetworkCurve = NetworkCurve::Secp256k1;
fn prepare_network_data() -> (u32, NetworkData) {
(
1u32,
ghost_helpers::networks::NetworkDataBuilder::default()
.with_gatekeeper(
hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.expect("Invalid public key"),
)
.with_default_endpoints(vec![
b"https://some-mainnet.infura.io/v3/demo-key".to_vec(),
b"https://rpc4ever.info/api".to_vec(),
])
.with_selector(vec![1u8; 4])
.with_network_type(NetworkType::Evm)
.with_network_curve(CURRENT_CURVE)
.with_rate_limit_delay(6)
.with_finality_delay(69)
.with_block_deviation(420)
.with_incoming_fee(0)
.with_outgoing_fee(0)
.build(),
NetworkData {
chain_name: "Ethereum".into(),
default_endpoints: vec![
"https:://some-endpoint.my-server.com/v1/my-super-secret-key".into(),
"https:://another-endpoint.my-server.com/v1/my-super-secret-key".into(),
],
finality_delay: 69,
rate_limit_delay: 69,
block_distance: 69,
avg_block_speed: 12_000,
network_type: NetworkType::Evm,
gatekeeper: b"0x1234567891234567891234567891234567891234".to_vec(),
topic_name: b"0x12345678912345678912345678912345678912345678912345678912345678"
.to_vec(),
incoming_fee: 0,
outgoing_fee: 0,
},
)
}
fn register_and_check_network(chain_id: u32, network: NetworkData) {
let network_curve = network.curve;
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
@ -42,8 +38,6 @@ fn register_and_check_network(chain_id: u32, network: NetworkData) {
));
assert_eq!(Networks::<Test>::get(chain_id), Some(network));
assert_eq!(NetworkIndexes::<Test>::get(), vec![chain_id]);
assert!(NetworkCurves::<Test>::contains_key(&network_curve));
assert!(NetworkCurvesVec::<Test>::get().contains(&network_curve));
}
#[test]
@ -52,21 +46,19 @@ fn could_add_network_from_authority() {
let (chain_id, network) = prepare_network_data();
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
network.clone(),
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkRegistered { chain_id },
crate::Event::NetworkRegistered {
chain_id,
network: network.clone(),
},
));
assert_eq!(Networks::<Test>::get(chain_id), Some(network));
assert_eq!(NetworkIndexes::<Test>::get(), vec![chain_id]);
assert!(NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
});
}
@ -76,9 +68,6 @@ fn could_not_add_network_from_random_account() {
let (chain_id, network) = prepare_network_data();
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
assert_err!(
GhostNetworks::register_network(
RuntimeOrigin::signed(RandomAccount::get()),
@ -105,29 +94,28 @@ fn could_not_add_network_from_random_account() {
);
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
});
}
#[test]
fn could_update_network_selector_from_authority_account() {
fn could_update_network_name_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let new_selector =
BoundedVec::<u8, ConstU32<MAX_SELECTOR_LEN>>::try_from(vec![69, 69, 69, 69])
.unwrap();
let new_name = b"Polygon".to_vec();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_selector(
assert_ok!(GhostNetworks::update_network_name(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
new_selector.clone()
new_name.clone()
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkSelectorUpdated { chain_id },
crate::Event::NetworkNameUpdated {
chain_id,
chain_name: new_name.clone(),
},
));
let mut final_network = network.clone();
final_network.selector = new_selector;
final_network.chain_name = new_name;
assert_eq!(Networks::<Test>::get(chain_id), Some(final_network.clone()));
assert_ne!(network, final_network);
});
@ -136,10 +124,8 @@ fn could_update_network_selector_from_authority_account() {
#[test]
fn could_add_network_endpoint_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let raw_endpoint =
BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let raw_endpoint: Vec<u8> =
"https:://new-endpoint.my-server.com/v1/my-super-secret-key".into();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_endpoint(
@ -149,7 +135,10 @@ fn could_add_network_endpoint_from_authority_account() {
Some(raw_endpoint.clone()),
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkEndpointAdded { chain_id },
crate::Event::NetworkEndpointAdded {
chain_id,
endpoint: raw_endpoint.clone(),
},
));
let current_network = Networks::<Test>::get(chain_id).unwrap();
assert_eq!(
@ -205,10 +194,8 @@ fn could_remove_network_endpoint_from_authority_account() {
fn could_update_network_endpoint_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let index_to_update = 0u32;
let raw_endpoint =
BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let raw_endpoint: Vec<u8> =
"https:://new-endpoint.my-server.com/v1/my-super-secret-key".into();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_endpoint(
@ -218,7 +205,11 @@ fn could_update_network_endpoint_from_authority_account() {
Some(raw_endpoint.clone()),
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkEndpointUpdated { chain_id, index: index_to_update },
crate::Event::NetworkEndpointUpdated {
chain_id,
index: index_to_update,
endpoint: raw_endpoint.clone(),
},
));
let previous_endpoints_len = network.default_endpoints.len();
let mut final_network = network.clone();
@ -294,24 +285,24 @@ fn could_update_network_rate_limit_delay_from_authority_account() {
}
#[test]
fn could_update_network_block_deviation_from_authority_account() {
fn could_update_network_block_distance_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let new_block_deviation = 1337;
let new_block_distance = 1337;
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_block_deviation(
assert_ok!(GhostNetworks::update_network_block_distance(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
new_block_deviation
new_block_distance
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkBlockDeviationUpdated {
crate::Event::NetworkBlockDistanceUpdated {
chain_id,
block_deviation: new_block_deviation,
block_distance: new_block_distance,
},
));
let mut final_network = network.clone();
final_network.block_deviation = new_block_deviation;
final_network.block_distance = new_block_distance;
assert_eq!(Networks::<Test>::get(chain_id), Some(final_network.clone()));
assert_ne!(network, final_network);
});
@ -335,43 +326,7 @@ fn could_update_network_type_from_authority_account() {
},
));
let mut final_network = network.clone();
final_network.r#type = new_type;
assert_eq!(Networks::<Test>::get(chain_id), Some(final_network.clone()));
assert_ne!(network, final_network);
});
}
#[test]
fn could_update_network_curve_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let new_curve = NetworkCurve::Ed25519;
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert!(!NetworkCurves::<Test>::contains_key(&new_curve));
assert!(NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&new_curve));
assert!(NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
assert_ok!(GhostNetworks::update_network_curve(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
new_curve.clone()
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkCurveUpdated {
chain_id,
network_curve: new_curve.clone(),
},
));
let mut final_network = network.clone();
final_network.r#curve = new_curve;
assert!(NetworkCurves::<Test>::contains_key(&new_curve));
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(NetworkCurvesVec::<Test>::get().contains(&new_curve));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
final_network.network_type = new_type;
assert_eq!(Networks::<Test>::get(chain_id), Some(final_network.clone()));
assert_ne!(network, final_network);
});
@ -380,10 +335,7 @@ fn could_update_network_curve_from_authority_account() {
#[test]
fn could_update_network_gatekeeper_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let new_gatekeeper = BoundedVec::<u8, ConstU32<MAX_GATEKEEPER_LEN>>::try_from(
hex::decode("04b2c7e3d9f9f8a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0")
.expect("Invalid public key"),
).unwrap();
let new_gatekeeper = b"0x9876543219876543219876543219876543219876".to_vec();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_gatekeeper(
@ -392,7 +344,10 @@ fn could_update_network_gatekeeper_from_authority_account() {
new_gatekeeper.clone()
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkGatekeeperUpdated { chain_id },
crate::Event::NetworkGatekeeperUpdated {
chain_id,
gatekeeper: new_gatekeeper.clone(),
},
));
let mut final_network = network.clone();
final_network.gatekeeper = new_gatekeeper;
@ -401,6 +356,31 @@ fn could_update_network_gatekeeper_from_authority_account() {
});
}
#[test]
fn could_update_network_topic_name_from_authority_account() {
ExtBuilder::build().execute_with(|| {
let new_topic_name =
b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_ok!(GhostNetworks::update_network_topic_name(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
new_topic_name.clone()
));
System::assert_last_event(RuntimeEvent::GhostNetworks(
crate::Event::NetworkTopicNameUpdated {
chain_id,
topic_name: new_topic_name.clone(),
},
));
let mut final_network = network.clone();
final_network.topic_name = new_topic_name;
assert_eq!(Networks::<Test>::get(chain_id), Some(final_network.clone()));
assert_ne!(network, final_network);
});
}
#[test]
fn could_update_incoming_network_fee_from_authority_account() {
ExtBuilder::build().execute_with(|| {
@ -449,13 +429,45 @@ fn could_update_outgoing_network_fee_from_authority_account() {
});
}
#[test]
fn could_not_update_network_name_from_random_account() {
ExtBuilder::build().execute_with(|| {
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_err!(
GhostNetworks::update_network_name(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
"Binance".into()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_name(
RuntimeOrigin::signed(RemoverAccount::get()),
chain_id,
"Binance".into()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_name(
RuntimeOrigin::signed(RandomAccount::get()),
chain_id,
"Binance".into()
),
DispatchError::BadOrigin
);
assert_eq!(Networks::<Test>::get(chain_id), Some(network));
});
}
#[test]
fn could_not_update_network_endpoint_from_random_account() {
ExtBuilder::build().execute_with(|| {
let index_to_update = 0u32;
let raw_endpoint = BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let raw_endpoint: Vec<u8> =
"https:://new-endpoint.my-server.com/v1/my-super-secret-key".into();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_err!(
@ -492,9 +504,8 @@ fn could_not_update_network_endpoint_from_random_account() {
#[test]
fn could_not_add_network_endpoint_from_random_account() {
ExtBuilder::build().execute_with(|| {
let raw_endpoint = BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let raw_endpoint: Vec<u8> =
"https:://new-endpoint.my-server.com/v1/my-super-secret-key".into();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_err!(
@ -637,29 +648,29 @@ fn could_not_update_network_rate_limit_delay_from_random_account() {
fn could_not_update_network_release_delay_from_random_account() {
ExtBuilder::build().execute_with(|| {
let (chain_id, network) = prepare_network_data();
let block_deviation = 1337;
let block_distance = 1337;
register_and_check_network(chain_id, network.clone());
assert_err!(
GhostNetworks::update_network_block_deviation(
GhostNetworks::update_network_block_distance(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
block_deviation
block_distance
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_block_deviation(
GhostNetworks::update_network_block_distance(
RuntimeOrigin::signed(RemoverAccount::get()),
chain_id,
block_deviation
block_distance
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_block_deviation(
GhostNetworks::update_network_block_distance(
RuntimeOrigin::signed(RandomAccount::get()),
chain_id,
block_deviation
block_distance
),
DispatchError::BadOrigin
);
@ -701,31 +712,31 @@ fn could_not_update_network_type_from_random_account() {
}
#[test]
fn could_not_update_network_curve_from_random_account() {
fn could_not_update_network_gatekeeper_from_random_account() {
ExtBuilder::build().execute_with(|| {
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_err!(
GhostNetworks::update_network_curve(
GhostNetworks::update_network_gatekeeper(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
NetworkCurve::Ed25519,
b"0x9876543219876543219876543219876543219876".to_vec()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_curve(
GhostNetworks::update_network_gatekeeper(
RuntimeOrigin::signed(RemoverAccount::get()),
chain_id,
NetworkCurve::Ed25519,
b"0x9876543219876543219876543219876543219876".to_vec()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_curve(
GhostNetworks::update_network_gatekeeper(
RuntimeOrigin::signed(RandomAccount::get()),
chain_id,
NetworkCurve::Ed25519,
b"0x9876543219876543219876543219876543219876".to_vec()
),
DispatchError::BadOrigin
);
@ -734,35 +745,31 @@ fn could_not_update_network_curve_from_random_account() {
}
#[test]
fn could_not_update_network_gatekeeper_from_random_account() {
fn could_not_update_network_topic_name_from_random_account() {
ExtBuilder::build().execute_with(|| {
let new_gatekeeper = BoundedVec::<u8, ConstU32<MAX_GATEKEEPER_LEN>>::try_from(
hex::decode("04b2c7e3d9f9f8a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0")
.expect("Invalid public key"),
).unwrap();
let (chain_id, network) = prepare_network_data();
register_and_check_network(chain_id, network.clone());
assert_err!(
GhostNetworks::update_network_gatekeeper(
GhostNetworks::update_network_topic_name(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
new_gatekeeper.clone(),
b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_gatekeeper(
GhostNetworks::update_network_topic_name(
RuntimeOrigin::signed(RemoverAccount::get()),
chain_id,
new_gatekeeper.clone(),
b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec()
),
DispatchError::BadOrigin
);
assert_err!(
GhostNetworks::update_network_gatekeeper(
GhostNetworks::update_network_topic_name(
RuntimeOrigin::signed(RandomAccount::get()),
chain_id,
new_gatekeeper,
b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec()
),
DispatchError::BadOrigin
);
@ -836,12 +843,26 @@ fn could_not_update_network_outgoing_fee_from_random_account() {
});
}
#[test]
fn could_not_update_name_for_non_existent_network() {
ExtBuilder::build().execute_with(|| {
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
GhostNetworks::update_network_name(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
"Binance".into()
),
crate::Error::<Test>::NetworkDoesNotExist
);
assert_eq!(Networks::<Test>::get(chain_id), None);
});
}
#[test]
fn could_not_update_endpoint_for_non_existent_network() {
ExtBuilder::build().execute_with(|| {
let raw_endpoint = BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
@ -849,7 +870,7 @@ fn could_not_update_endpoint_for_non_existent_network() {
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
Some(0u32),
Some(raw_endpoint),
Some("https:://new-endpoint.my-server.com/v1/my-super-secret-key".into()),
),
crate::Error::<Test>::NetworkDoesNotExist
);
@ -860,9 +881,6 @@ fn could_not_update_endpoint_for_non_existent_network() {
#[test]
fn could_not_add_endpoint_for_non_existent_network() {
ExtBuilder::build().execute_with(|| {
let raw_endpoint = BoundedVec::<u8, ConstU32<MAX_ENDPOINT_LEN>>::try_from(
b"https:://new-endpoint.my-server.com/v1/my-super-secret-key".to_vec(),
).unwrap();
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
@ -870,7 +888,7 @@ fn could_not_add_endpoint_for_non_existent_network() {
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
None,
Some(raw_endpoint),
Some("https:://new-endpoint.my-server.com/v1/my-super-secret-key".into()),
),
crate::Error::<Test>::NetworkDoesNotExist
);
@ -936,7 +954,7 @@ fn could_not_update_release_delay_for_non_existent_network() {
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
GhostNetworks::update_network_block_deviation(
GhostNetworks::update_network_block_distance(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
1337
@ -965,15 +983,15 @@ fn could_not_update_type_for_non_existent_network() {
}
#[test]
fn could_not_update_curve_for_non_existent_network() {
fn could_not_update_gatekeeper_for_non_existent_network() {
ExtBuilder::build().execute_with(|| {
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
GhostNetworks::update_network_curve(
GhostNetworks::update_network_gatekeeper(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
NetworkCurve::Ed25519,
b"0x9876543219876543219876543219876543219876".to_vec()
),
crate::Error::<Test>::NetworkDoesNotExist
);
@ -982,19 +1000,15 @@ fn could_not_update_curve_for_non_existent_network() {
}
#[test]
fn could_not_update_gatekeeper_for_non_existent_network() {
fn could_not_update_topic_name_for_non_existent_network() {
ExtBuilder::build().execute_with(|| {
let new_gatekeeper = BoundedVec::<u8, ConstU32<MAX_GATEKEEPER_LEN>>::try_from(
hex::decode("04b2c7e3d9f9f8a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0")
.expect("Invalid public key"),
).unwrap();
let chain_id: u32 = 1;
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_err!(
GhostNetworks::update_network_gatekeeper(
GhostNetworks::update_network_topic_name(
RuntimeOrigin::signed(UpdaterAccount::get()),
chain_id,
new_gatekeeper,
b"0x9876543219876543219876543219876543219876543219876543219876543219".to_vec()
),
crate::Error::<Test>::NetworkDoesNotExist
);
@ -1047,8 +1061,6 @@ fn could_remove_network_from_authority_account() {
));
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
});
}
@ -1074,8 +1086,6 @@ fn could_not_remove_network_from_random_account() {
);
assert_eq!(Networks::<Test>::get(chain_id), Some(network));
assert_eq!(NetworkIndexes::<Test>::get(), vec![chain_id]);
assert!(NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
});
}
@ -1090,8 +1100,6 @@ fn could_not_remove_non_existent_network() {
);
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
assert!(!NetworkCurves::<Test>::contains_key(&CURRENT_CURVE));
assert!(!NetworkCurvesVec::<Test>::get().contains(&CURRENT_CURVE));
});
}
@ -1764,6 +1772,52 @@ fn check_bridged_inflation_curve_for_big_commissions() {
});
}
#[test]
fn migration_from_v0_to_v1_works() {
ExtBuilder::build().execute_with(|| {
let (chain_id, network) = prepare_network_data();
let other_chain_id = chain_id.saturating_add(1);
let removed_pallet_name = <Pallet<Test> as PalletInfoAccess>::name();
let removed_storage = "NullifyNeeded";
let key = [
sp_io::hashing::twox_128(removed_pallet_name.as_bytes()).to_vec(),
sp_io::hashing::twox_128(removed_storage.as_bytes()).to_vec(),
]
.concat();
frame_support::storage::unhashed::put_raw(&key, &true.encode());
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
network.clone(),
));
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
other_chain_id,
network.clone(),
));
NetworkIndexes::<Test>::kill();
assert!(frame_support::storage::unhashed::exists(&key));
assert_eq!(Networks::<Test>::get(chain_id), Some(network.clone()));
assert_eq!(Networks::<Test>::get(other_chain_id), Some(network));
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
type Migrate = crate::migrations::MigrateV0ToV1<Test>;
<Migrate as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade();
assert!(!frame_support::storage::unhashed::exists(&key));
assert_eq!(
NetworkIndexes::<Test>::get(),
vec![chain_id, other_chain_id]
);
});
}
#[test]
fn error_on_max_networks_overflow() {
ExtBuilder::build().execute_with(|| {
@ -1789,3 +1843,34 @@ fn error_on_max_networks_overflow() {
);
});
}
#[test]
fn migration_from_v0_to_v1_does_not_run_twice() {
ExtBuilder::build().execute_with(|| {
StorageVersion::new(1).put::<GhostNetworks>();
let (chain_id, network) = prepare_network_data();
let other_chain_id = chain_id.saturating_add(1);
assert_eq!(Networks::<Test>::get(chain_id), None);
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
chain_id,
network.clone(),
));
assert_ok!(GhostNetworks::register_network(
RuntimeOrigin::signed(RegistererAccount::get()),
other_chain_id,
network.clone(),
));
NetworkIndexes::<Test>::kill();
assert_eq!(Networks::<Test>::get(chain_id), Some(network.clone()));
assert_eq!(Networks::<Test>::get(other_chain_id), Some(network));
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
type Migrate = crate::migrations::MigrateV0ToV1<Test>;
<Migrate as frame_support::traits::OnRuntimeUpgrade>::on_runtime_upgrade();
assert_eq!(NetworkIndexes::<Test>::get(), vec![]);
});
}

View File

@ -49,13 +49,12 @@ use sp_std::marker::PhantomData;
/// Weight functions needed for ghost_networks
pub trait WeightInfo {
fn register_network(i: u32, j: u32, k: u32, ) -> Weight;
fn update_network_selector() -> Weight;
fn update_network_name(n: u32, ) -> Weight;
fn update_network_endpoint(n: u32, ) -> Weight;
fn update_network_finality_delay() -> Weight;
fn update_network_rate_limit_delay() -> Weight;
fn update_network_block_deviation() -> Weight;
fn update_network_block_distance() -> Weight;
fn update_network_type() -> Weight;
fn update_network_curve() -> Weight;
fn update_network_gatekeeper() -> Weight;
fn update_network_topic_name() -> Weight;
fn update_incoming_network_fee() -> Weight;
@ -89,7 +88,7 @@ impl WeightInfo for () {
/// Storage: `GhostNetworks::Networks` (r:1 w:1)
/// Proof: `GhostNetworks::Networks` (`max_values`: None, `max_size`: None, mode: `Measured`)
/// The range of component `n` is `[1, 20]`.
fn update_network_selector() -> Weight {
fn update_network_name(_n: u32, ) -> Weight {
// Proof Size summary in bytes:
// Measured: `339`
// Estimated: `3804`
@ -140,7 +139,7 @@ impl WeightInfo for () {
}
/// Storage: `GhostNetworks::Networks` (r:1 w:1)
/// Proof: `GhostNetworks::Networks` (`max_values`: None, `max_size`: None, mode: `Measured`)
fn update_network_block_deviation() -> Weight {
fn update_network_block_distance() -> Weight {
// Proof Size summary in bytes:
// Measured: `339`
// Estimated: `3804`
@ -162,9 +161,6 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().reads(1))
.saturating_add(RocksDbWeight::get().writes(1))
}
fn update_network_curve() -> Weight {
Default::default()
}
/// Storage: `GhostNetworks::Networks` (r:1 w:1)
/// Proof: `GhostNetworks::Networks` (`max_values`: None, `max_size`: None, mode: `Measured`)
fn update_network_gatekeeper() -> Weight {

View File

@ -1,63 +0,0 @@
[package]
name = "ghost-nomination-pools"
version = "25.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "Fork of FRAME nomination pools pallet from Parity"
[dependencies]
# parity
codec = { workspace = true, features = ["derive"] }
scale-info = { workspace = true, features = ["derive"] }
# FRAME
frame-support = { workspace = true, default-features = false }
frame-system = { workspace = true, default-features = false }
sp-runtime = { workspace = true, default-features = false }
sp-std = { workspace = true, default-features = false }
sp-staking = { workspace = true, default-features = false }
sp-core = { workspace = true, default-features = false }
sp-io = { workspace = true, default-features = false }
log = { workspace = true }
# Optional: use for testing and/or fuzzing
pallet-balances = { workspace = true, optional = true, default-features = false }
sp-tracing = { workspace = true, optional = true, default-features = false }
[dev-dependencies]
pallet-balances = { workspace = true }
sp-tracing = { workspace = true }
[features]
default = ["std"]
fuzzing = ["pallet-balances", "sp-tracing"]
std = [
"codec/std",
"frame-support/std",
"frame-system/std",
"log/std",
"pallet-balances?/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-staking/std",
"sp-std/std",
"sp-tracing?/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances?/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances?/try-runtime",
"sp-runtime/try-runtime",
]

View File

@ -1,74 +0,0 @@
[package]
name = "ghost-nomination-pools-benchmarking"
version = "26.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "Fork of FRAME nomination pools pallet benchmarking"
readme = "README.md"
[dependencies]
# parity
codec = { workspace = true, default-features = false, features = ["derive"] }
scale-info = { workspace = true, default-features = false, features = ["derive"] }
# FRAME
frame-benchmarking = { workspace = true, default-features = false }
frame-election-provider-support = { workspace = true, default-features = false }
frame-support = { workspace = true, default-features = false }
frame-system = { workspace = true, default-features = false }
pallet-bags-list = { workspace = true, default-features = false }
pallet-staking = { workspace = true, default-features = false }
ghost-nomination-pools = { workspace = true, default-features = false }
# Substrate Primitives
sp-runtime = { workspace = true, default-features = false }
sp-runtime-interface = { workspace = true, default-features = false }
sp-staking = { workspace = true, default-features = false }
sp-std = { workspace = true, default-features = false }
[dev-dependencies]
pallet-balances = { workspace = true, default-features = false }
pallet-timestamp = { workspace = true }
pallet-staking-reward-curve = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking/std",
"frame-election-provider-support/std",
"frame-support/std",
"frame-system/std",
"pallet-bags-list/std",
"pallet-balances/std",
"ghost-nomination-pools/std",
"pallet-staking/std",
"pallet-timestamp/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-runtime-interface/std",
"sp-runtime/std",
"sp-staking/std",
"sp-std/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-election-provider-support/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-bags-list/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"ghost-nomination-pools/runtime-benchmarks",
"pallet-staking/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]

View File

@ -1,846 +0,0 @@
// This file is part of Substrate.
// 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.
//! Benchmarks for the nomination pools coupled with the staking and bags list pallets.
use frame_benchmarking::v1::{account, whitelist_account};
use frame_election_provider_support::SortedListProvider;
use frame_support::{
assert_ok, ensure,
traits::{
fungible::{Inspect, Mutate, Unbalanced},
Get,
},
};
use frame_system::RawOrigin as RuntimeOrigin;
use ghost_nomination_pools::{
BalanceOf, BondExtra, BondedPoolInner, BondedPools, ClaimPermission, ClaimPermissions,
Commission, CommissionChangeRate, CommissionClaimPermission, ConfigOp, GlobalMaxCommission,
MaxPoolMembers, MaxPoolMembersPerPool, MaxPools, Metadata, MinCreateBond, MinJoinBond,
Pallet as Pools, PoolMembers, PoolRoles, PoolState, RewardPools, SubPoolsStorage,
};
use pallet_staking::MaxNominationsOf;
use sp_runtime::{
traits::{Bounded, StaticLookup, Zero},
Perbill,
};
use sp_staking::{EraIndex, StakingInterface};
use sp_std::{vec, vec::Vec};
// `frame_benchmarking::benchmarks!` macro needs this
use ghost_nomination_pools::Call;
type CurrencyOf<T> = <T as ghost_nomination_pools::Config>::Currency;
const USER_SEED: u32 = 0;
const MAX_SPANS: u32 = 100;
pub(crate) type VoterBagsListInstance = pallet_bags_list::Instance1;
pub trait Config:
ghost_nomination_pools::Config
+ pallet_staking::Config
+ pallet_bags_list::Config<VoterBagsListInstance>
{
}
pub struct Pallet<T: Config>(Pools<T>);
fn create_funded_user_with_balance<T: ghost_nomination_pools::Config>(
string: &'static str,
n: u32,
balance: BalanceOf<T>,
) -> T::AccountId {
let user = account(string, n, USER_SEED);
T::Currency::set_balance(&user, balance);
user
}
// Create a bonded pool account, bonding `balance` and giving the account `balance * 2` free
// balance.
fn create_pool_account<T: ghost_nomination_pools::Config>(
n: u32,
balance: BalanceOf<T>,
commission: Option<Perbill>,
) -> (T::AccountId, T::AccountId) {
let ed = CurrencyOf::<T>::minimum_balance();
let pool_creator: T::AccountId =
create_funded_user_with_balance::<T>("pool_creator", n, ed + balance * 2u32.into());
let pool_creator_lookup = T::Lookup::unlookup(pool_creator.clone());
Pools::<T>::create(
RuntimeOrigin::Signed(pool_creator.clone()).into(),
balance,
pool_creator_lookup.clone(),
pool_creator_lookup.clone(),
pool_creator_lookup,
)
.unwrap();
if let Some(c) = commission {
let pool_id = ghost_nomination_pools::LastPoolId::<T>::get();
Pools::<T>::set_commission(
RuntimeOrigin::Signed(pool_creator.clone()).into(),
pool_id,
Some((c, pool_creator.clone())),
)
.expect("pool just created, commission can be set by root; qed");
}
let pool_account = ghost_nomination_pools::BondedPools::<T>::iter()
.find(|(_, bonded_pool)| bonded_pool.roles.depositor == pool_creator)
.map(|(pool_id, _)| Pools::<T>::create_bonded_account(pool_id))
.expect("pool_creator created a pool above");
(pool_creator, pool_account)
}
fn vote_to_balance<T: ghost_nomination_pools::Config>(
vote: u64,
) -> Result<BalanceOf<T>, &'static str> {
vote.try_into().map_err(|_| "could not convert u64 to Balance")
}
#[allow(unused)]
struct ListScenario<T: ghost_nomination_pools::Config> {
/// Stash/Controller that is expected to be moved.
origin1: T::AccountId,
creator1: T::AccountId,
dest_weight: BalanceOf<T>,
origin1_member: Option<T::AccountId>,
}
impl<T: Config> ListScenario<T> {
/// An expensive scenario for bags-list implementation:
///
/// - the node to be updated (r) is the head of a bag that has at least one other node. The bag
/// itself will need to be read and written to update its head. The node pointed to by r.next
/// will need to be read and written as it will need to have its prev pointer updated. Note
/// that there are two other worst case scenarios for bag removal: 1) the node is a tail and
/// 2) the node is a middle node with prev and next; all scenarios end up with the same number
/// of storage reads and writes.
///
/// - the destination bag has at least one node, which will need its next pointer updated.
pub(crate) fn new(
origin_weight: BalanceOf<T>,
is_increase: bool,
) -> Result<Self, &'static str> {
ensure!(!origin_weight.is_zero(), "origin weight must be greater than 0");
ensure!(
ghost_nomination_pools::MaxPools::<T>::get().unwrap_or(0) >= 3,
"must allow at least three pools for benchmarks"
);
// Burn the entire issuance.
CurrencyOf::<T>::set_total_issuance(Zero::zero());
// Create accounts with the origin weight
let (pool_creator1, pool_origin1) =
create_pool_account::<T>(USER_SEED + 1, origin_weight, Some(Perbill::from_percent(50)));
T::Staking::nominate(
&pool_origin1,
// NOTE: these don't really need to be validators.
vec![account("random_validator", 0, USER_SEED)],
)?;
let (_, pool_origin2) =
create_pool_account::<T>(USER_SEED + 2, origin_weight, Some(Perbill::from_percent(50)));
T::Staking::nominate(
&pool_origin2,
vec![account("random_validator", 0, USER_SEED)].clone(),
)?;
// Find a destination weight that will trigger the worst case scenario
let dest_weight_as_vote = <T as pallet_staking::Config>::VoterList::score_update_worst_case(
&pool_origin1,
is_increase,
);
let dest_weight: BalanceOf<T> =
dest_weight_as_vote.try_into().map_err(|_| "could not convert u64 to Balance")?;
// Create an account with the worst case destination weight
let (_, pool_dest1) =
create_pool_account::<T>(USER_SEED + 3, dest_weight, Some(Perbill::from_percent(50)));
T::Staking::nominate(&pool_dest1, vec![account("random_validator", 0, USER_SEED)])?;
let weight_of = pallet_staking::Pallet::<T>::weight_of_fn();
assert_eq!(vote_to_balance::<T>(weight_of(&pool_origin1)).unwrap(), origin_weight);
assert_eq!(vote_to_balance::<T>(weight_of(&pool_origin2)).unwrap(), origin_weight);
assert_eq!(vote_to_balance::<T>(weight_of(&pool_dest1)).unwrap(), dest_weight);
Ok(ListScenario {
origin1: pool_origin1,
creator1: pool_creator1,
dest_weight,
origin1_member: None,
})
}
fn add_joiner(mut self, amount: BalanceOf<T>) -> Self {
let amount = MinJoinBond::<T>::get()
.max(CurrencyOf::<T>::minimum_balance())
// Max `amount` with minimum thresholds for account balance and joining a pool
// to ensure 1) the user can be created and 2) can join the pool
.max(amount);
let joiner: T::AccountId = account("joiner", USER_SEED, 0);
self.origin1_member = Some(joiner.clone());
CurrencyOf::<T>::set_balance(&joiner, amount * 2u32.into());
let original_bonded = T::Staking::active_stake(&self.origin1).unwrap();
// Unbond `amount` from the underlying pool account so when the member joins
// we will maintain `current_bonded`.
T::Staking::unbond(&self.origin1, amount).expect("the pool was created in `Self::new`.");
// Account pool points for the unbonded balance.
BondedPools::<T>::mutate(&1, |maybe_pool| {
maybe_pool.as_mut().map(|pool| pool.points -= amount)
});
Pools::<T>::join(RuntimeOrigin::Signed(joiner.clone()).into(), amount, 1).unwrap();
// check that the vote weight is still the same as the original bonded
let weight_of = pallet_staking::Pallet::<T>::weight_of_fn();
assert_eq!(vote_to_balance::<T>(weight_of(&self.origin1)).unwrap(), original_bonded);
// check the member was added correctly
let member = PoolMembers::<T>::get(&joiner).unwrap();
assert_eq!(member.points, amount);
assert_eq!(member.pool_id, 1);
self
}
}
frame_benchmarking::benchmarks! {
join {
let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
// setup the worst case list scenario.
let scenario = ListScenario::<T>::new(origin_weight, true)?;
assert_eq!(
T::Staking::active_stake(&scenario.origin1).unwrap(),
origin_weight
);
let max_additional = scenario.dest_weight - origin_weight;
let joiner_free = CurrencyOf::<T>::minimum_balance() + max_additional;
let joiner: T::AccountId
= create_funded_user_with_balance::<T>("joiner", 0, joiner_free);
whitelist_account!(joiner);
}: _(RuntimeOrigin::Signed(joiner.clone()), max_additional, 1)
verify {
assert_eq!(CurrencyOf::<T>::balance(&joiner), joiner_free - max_additional);
assert_eq!(
T::Staking::active_stake(&scenario.origin1).unwrap(),
scenario.dest_weight
);
}
bond_extra_transfer {
let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let extra = scenario.dest_weight - origin_weight;
// creator of the src pool will bond-extra, bumping itself to dest bag.
}: bond_extra(RuntimeOrigin::Signed(scenario.creator1.clone()), BondExtra::FreeBalance(extra))
verify {
assert!(
T::Staking::active_stake(&scenario.origin1).unwrap() >=
scenario.dest_weight
);
}
bond_extra_other {
let claimer: T::AccountId = account("claimer", USER_SEED + 4, 0);
let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
let scenario = ListScenario::<T>::new(origin_weight, true)?;
let extra = (scenario.dest_weight - origin_weight).max(CurrencyOf::<T>::minimum_balance());
// set claim preferences to `PermissionlessAll` to any account to bond extra on member's behalf.
let _ = Pools::<T>::set_claim_permission(RuntimeOrigin::Signed(scenario.creator1.clone()).into(), ClaimPermission::PermissionlessAll);
// transfer exactly `extra` to the depositor of the src pool (1),
let reward_account1 = Pools::<T>::create_reward_account(1);
assert!(extra >= CurrencyOf::<T>::minimum_balance());
let _ = CurrencyOf::<T>::mint_into(&reward_account1, extra);
}: _(RuntimeOrigin::Signed(claimer), T::Lookup::unlookup(scenario.creator1.clone()), BondExtra::Rewards)
verify {
// commission of 50% deducted here.
assert!(
T::Staking::active_stake(&scenario.origin1).unwrap() >=
scenario.dest_weight / 2u32.into()
);
}
claim_payout {
let claimer: T::AccountId = account("claimer", USER_SEED + 4, 0);
let commission = Perbill::from_percent(50);
let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
let ed = CurrencyOf::<T>::minimum_balance();
let (depositor, pool_account) = create_pool_account::<T>(0, origin_weight, Some(commission));
let reward_account = Pools::<T>::create_reward_account(1);
// Send funds to the reward account of the pool
CurrencyOf::<T>::set_balance(&reward_account, ed + origin_weight);
// set claim preferences to `PermissionlessAll` so any account can claim rewards on member's
// behalf.
let _ = Pools::<T>::set_claim_permission(RuntimeOrigin::Signed(depositor.clone()).into(), ClaimPermission::PermissionlessAll);
// Sanity check
assert_eq!(
CurrencyOf::<T>::balance(&depositor),
origin_weight
);
whitelist_account!(depositor);
}:claim_payout_other(RuntimeOrigin::Signed(claimer), depositor.clone())
verify {
assert_eq!(
CurrencyOf::<T>::balance(&depositor),
origin_weight + commission * origin_weight
);
assert_eq!(
CurrencyOf::<T>::balance(&reward_account),
ed + commission * origin_weight
);
}
unbond {
// The weight the nominator will start at. The value used here is expected to be
// significantly higher than the first position in a list (e.g. the first bag threshold).
let origin_weight = Pools::<T>::depositor_min_bond() * 200u32.into();
let scenario = ListScenario::<T>::new(origin_weight, false)?;
let amount = origin_weight - scenario.dest_weight;
let scenario = scenario.add_joiner(amount);
let member_id = scenario.origin1_member.unwrap().clone();
let member_id_lookup = T::Lookup::unlookup(member_id.clone());
let all_points = PoolMembers::<T>::get(&member_id).unwrap().points;
whitelist_account!(member_id);
}: _(RuntimeOrigin::Signed(member_id.clone()), member_id_lookup, all_points)
verify {
let bonded_after = T::Staking::active_stake(&scenario.origin1).unwrap();
// We at least went down to the destination bag
assert!(bonded_after <= scenario.dest_weight);
let member = PoolMembers::<T>::get(
&member_id
)
.unwrap();
assert_eq!(
member.unbonding_eras.keys().cloned().collect::<Vec<_>>(),
vec![0 + T::Staking::bonding_duration()]
);
assert_eq!(
member.unbonding_eras.values().cloned().collect::<Vec<_>>(),
vec![all_points]
);
}
pool_withdraw_unbonded {
let s in 0 .. MAX_SPANS;
let min_create_bond = Pools::<T>::depositor_min_bond();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
// Add a new member
let min_join_bond = MinJoinBond::<T>::get().max(CurrencyOf::<T>::minimum_balance());
let joiner = create_funded_user_with_balance::<T>("joiner", 0, min_join_bond * 2u32.into());
Pools::<T>::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1)
.unwrap();
// Sanity check join worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond + min_join_bond
);
assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// Unbond the new member
Pools::<T>::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap();
// Sanity check that unbond worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond
);
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 1);
// Set the current era
pallet_staking::CurrentEra::<T>::put(EraIndex::max_value());
// Add `s` count of slashing spans to storage.
pallet_staking::benchmarking::add_slashing_spans::<T>(&pool_account, s);
whitelist_account!(pool_account);
}: _(RuntimeOrigin::Signed(pool_account.clone()), 1, s)
verify {
// The joiners funds didn't change
assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// The unlocking chunk was removed
assert_eq!(pallet_staking::Ledger::<T>::get(pool_account).unwrap().unlocking.len(), 0);
}
withdraw_unbonded_update {
let s in 0 .. MAX_SPANS;
let min_create_bond = Pools::<T>::depositor_min_bond();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
// Add a new member
let min_join_bond = MinJoinBond::<T>::get().max(CurrencyOf::<T>::minimum_balance());
let joiner = create_funded_user_with_balance::<T>("joiner", 0, min_join_bond * 2u32.into());
let joiner_lookup = T::Lookup::unlookup(joiner.clone());
Pools::<T>::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1)
.unwrap();
// Sanity check join worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond + min_join_bond
);
assert_eq!(CurrencyOf::<T>::balance(&joiner), min_join_bond);
// Unbond the new member
pallet_staking::CurrentEra::<T>::put(0);
Pools::<T>::fully_unbond(RuntimeOrigin::Signed(joiner.clone()).into(), joiner.clone()).unwrap();
// Sanity check that unbond worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond
);
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 1);
// Set the current era to ensure we can withdraw unbonded funds
pallet_staking::CurrentEra::<T>::put(EraIndex::max_value());
pallet_staking::benchmarking::add_slashing_spans::<T>(&pool_account, s);
whitelist_account!(joiner);
}: withdraw_unbonded(RuntimeOrigin::Signed(joiner.clone()), joiner_lookup, s)
verify {
assert_eq!(
CurrencyOf::<T>::balance(&joiner), min_join_bond * 2u32.into()
);
// The unlocking chunk was removed
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 0);
}
withdraw_unbonded_kill {
let s in 0 .. MAX_SPANS;
let min_create_bond = Pools::<T>::depositor_min_bond();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
let depositor_lookup = T::Lookup::unlookup(depositor.clone());
// We set the pool to the destroying state so the depositor can leave
BondedPools::<T>::try_mutate(&1, |maybe_bonded_pool| {
maybe_bonded_pool.as_mut().ok_or(()).map(|bonded_pool| {
bonded_pool.state = PoolState::Destroying;
})
})
.unwrap();
// Unbond the creator
pallet_staking::CurrentEra::<T>::put(0);
// Simulate some rewards so we can check if the rewards storage is cleaned up. We check this
// here to ensure the complete flow for destroying a pool works - the reward pool account
// should never exist by time the depositor withdraws so we test that it gets cleaned
// up when unbonding.
let reward_account = Pools::<T>::create_reward_account(1);
assert!(frame_system::Account::<T>::contains_key(&reward_account));
Pools::<T>::fully_unbond(RuntimeOrigin::Signed(depositor.clone()).into(), depositor.clone()).unwrap();
// Sanity check that unbond worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
Zero::zero()
);
assert_eq!(
CurrencyOf::<T>::balance(&pool_account),
min_create_bond
);
assert_eq!(pallet_staking::Ledger::<T>::get(&pool_account).unwrap().unlocking.len(), 1);
// Set the current era to ensure we can withdraw unbonded funds
pallet_staking::CurrentEra::<T>::put(EraIndex::max_value());
// Some last checks that storage items we expect to get cleaned up are present
assert!(pallet_staking::Ledger::<T>::contains_key(&pool_account));
assert!(BondedPools::<T>::contains_key(&1));
assert!(SubPoolsStorage::<T>::contains_key(&1));
assert!(RewardPools::<T>::contains_key(&1));
assert!(PoolMembers::<T>::contains_key(&depositor));
assert!(frame_system::Account::<T>::contains_key(&reward_account));
whitelist_account!(depositor);
}: withdraw_unbonded(RuntimeOrigin::Signed(depositor.clone()), depositor_lookup, s)
verify {
// Pool removal worked
assert!(!pallet_staking::Ledger::<T>::contains_key(&pool_account));
assert!(!BondedPools::<T>::contains_key(&1));
assert!(!SubPoolsStorage::<T>::contains_key(&1));
assert!(!RewardPools::<T>::contains_key(&1));
assert!(!PoolMembers::<T>::contains_key(&depositor));
assert!(!frame_system::Account::<T>::contains_key(&pool_account));
assert!(!frame_system::Account::<T>::contains_key(&reward_account));
// Funds where transferred back correctly
assert_eq!(
CurrencyOf::<T>::balance(&depositor),
// gets bond back + rewards collecting when unbonding
min_create_bond * 2u32.into() + CurrencyOf::<T>::minimum_balance()
);
}
create {
let min_create_bond = Pools::<T>::depositor_min_bond();
let depositor: T::AccountId = account("depositor", USER_SEED, 0);
let depositor_lookup = T::Lookup::unlookup(depositor.clone());
// Give the depositor some balance to bond
CurrencyOf::<T>::set_balance(&depositor, min_create_bond * 2u32.into());
// Make sure no Pools exist at a pre-condition for our verify checks
assert_eq!(RewardPools::<T>::count(), 0);
assert_eq!(BondedPools::<T>::count(), 0);
whitelist_account!(depositor);
}: _(
RuntimeOrigin::Signed(depositor.clone()),
min_create_bond,
depositor_lookup.clone(),
depositor_lookup.clone(),
depositor_lookup
)
verify {
assert_eq!(RewardPools::<T>::count(), 1);
assert_eq!(BondedPools::<T>::count(), 1);
let (_, new_pool) = BondedPools::<T>::iter().next().unwrap();
assert_eq!(
new_pool,
BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: min_create_bond,
roles: PoolRoles {
depositor: depositor.clone(),
root: Some(depositor.clone()),
nominator: Some(depositor.clone()),
bouncer: Some(depositor.clone()),
},
state: PoolState::Open,
}
);
assert_eq!(
T::Staking::active_stake(&Pools::<T>::create_bonded_account(1)),
Ok(min_create_bond)
);
}
nominate {
let n in 1 .. MaxNominationsOf::<T>::get();
// Create a pool
let min_create_bond = Pools::<T>::depositor_min_bond() * 2u32.into();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
// Create some accounts to nominate. For the sake of benchmarking they don't need to be
// actual validators
let validators: Vec<_> = (0..n)
.map(|i| account("stash", USER_SEED, i))
.collect();
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1, validators)
verify {
assert_eq!(RewardPools::<T>::count(), 1);
assert_eq!(BondedPools::<T>::count(), 1);
let (_, new_pool) = BondedPools::<T>::iter().next().unwrap();
assert_eq!(
new_pool,
BondedPoolInner {
commission: Commission::default(),
member_counter: 1,
points: min_create_bond,
roles: PoolRoles {
depositor: depositor.clone(),
root: Some(depositor.clone()),
nominator: Some(depositor.clone()),
bouncer: Some(depositor.clone()),
},
state: PoolState::Open,
}
);
assert_eq!(
T::Staking::active_stake(&Pools::<T>::create_bonded_account(1)),
Ok(min_create_bond)
);
}
set_state {
// Create a pool
let min_create_bond = Pools::<T>::depositor_min_bond();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
BondedPools::<T>::mutate(&1, |maybe_pool| {
// Force the pool into an invalid state
maybe_pool.as_mut().map(|pool| pool.points = min_create_bond * 10u32.into());
});
let caller = account("caller", 0, USER_SEED);
whitelist_account!(caller);
}:_(RuntimeOrigin::Signed(caller), 1, PoolState::Destroying)
verify {
assert_eq!(BondedPools::<T>::get(1).unwrap().state, PoolState::Destroying);
}
set_metadata {
let n in 1 .. <T as ghost_nomination_pools::Config>::MaxMetadataLen::get();
// Create a pool
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// Create metadata of the max possible size
let metadata: Vec<u8> = (0..n).map(|_| 42).collect();
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor), 1, metadata.clone())
verify {
assert_eq!(Metadata::<T>::get(&1), metadata);
}
set_configs {
}:_(
RuntimeOrigin::Root,
ConfigOp::Set(BalanceOf::<T>::max_value()),
ConfigOp::Set(BalanceOf::<T>::max_value()),
ConfigOp::Set(u32::MAX),
ConfigOp::Set(u32::MAX),
ConfigOp::Set(u32::MAX),
ConfigOp::Set(Perbill::max_value())
) verify {
assert_eq!(MinJoinBond::<T>::get(), BalanceOf::<T>::max_value());
assert_eq!(MinCreateBond::<T>::get(), BalanceOf::<T>::max_value());
assert_eq!(MaxPools::<T>::get(), Some(u32::MAX));
assert_eq!(MaxPoolMembers::<T>::get(), Some(u32::MAX));
assert_eq!(MaxPoolMembersPerPool::<T>::get(), Some(u32::MAX));
assert_eq!(GlobalMaxCommission::<T>::get(), Some(Perbill::max_value()));
}
update_roles {
let first_id = ghost_nomination_pools::LastPoolId::<T>::get() + 1;
let (root, _) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
let random: T::AccountId = account("but is anything really random in computers..?", 0, USER_SEED);
}:_(
RuntimeOrigin::Signed(root.clone()),
first_id,
ConfigOp::Set(random.clone()),
ConfigOp::Set(random.clone()),
ConfigOp::Set(random.clone())
) verify {
assert_eq!(
ghost_nomination_pools::BondedPools::<T>::get(first_id).unwrap().roles,
ghost_nomination_pools::PoolRoles {
depositor: root,
nominator: Some(random.clone()),
bouncer: Some(random.clone()),
root: Some(random),
},
)
}
chill {
// Create a pool
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// Nominate with the pool.
let validators: Vec<_> = (0..MaxNominationsOf::<T>::get())
.map(|i| account("stash", USER_SEED, i))
.collect();
assert_ok!(T::Staking::nominate(&pool_account, validators));
assert!(T::Staking::nominations(&Pools::<T>::create_bonded_account(1)).is_some());
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1)
verify {
assert!(T::Staking::nominations(&Pools::<T>::create_bonded_account(1)).is_none());
}
set_commission {
// Create a pool - do not set a commission yet.
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// set a max commission
Pools::<T>::set_commission_max(RuntimeOrigin::Signed(depositor.clone()).into(), 1u32.into(), Perbill::from_percent(50)).unwrap();
// set a change rate
Pools::<T>::set_commission_change_rate(RuntimeOrigin::Signed(depositor.clone()).into(), 1u32.into(), CommissionChangeRate {
max_increase: Perbill::from_percent(20),
min_delay: 0u32.into(),
}).unwrap();
// set a claim permission to an account.
Pools::<T>::set_commission_claim_permission(
RuntimeOrigin::Signed(depositor.clone()).into(),
1u32.into(),
Some(CommissionClaimPermission::Account(depositor.clone()))
).unwrap();
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Some((Perbill::from_percent(20), depositor.clone())))
verify {
assert_eq!(BondedPools::<T>::get(1).unwrap().commission, Commission {
current: Some((Perbill::from_percent(20), depositor.clone())),
max: Some(Perbill::from_percent(50)),
change_rate: Some(CommissionChangeRate {
max_increase: Perbill::from_percent(20),
min_delay: 0u32.into()
}),
throttle_from: Some(1u32.into()),
claim_permission: Some(CommissionClaimPermission::Account(depositor)),
});
}
set_commission_max {
// Create a pool, setting a commission that will update when max commission is set.
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), Some(Perbill::from_percent(50)));
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Perbill::from_percent(50))
verify {
assert_eq!(
BondedPools::<T>::get(1).unwrap().commission, Commission {
current: Some((Perbill::from_percent(50), depositor)),
max: Some(Perbill::from_percent(50)),
change_rate: None,
throttle_from: Some(0u32.into()),
claim_permission: None,
});
}
set_commission_change_rate {
// Create a pool
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), CommissionChangeRate {
max_increase: Perbill::from_percent(50),
min_delay: 1000u32.into(),
})
verify {
assert_eq!(
BondedPools::<T>::get(1).unwrap().commission, Commission {
current: None,
max: None,
change_rate: Some(CommissionChangeRate {
max_increase: Perbill::from_percent(50),
min_delay: 1000u32.into(),
}),
throttle_from: Some(1_u32.into()),
claim_permission: None,
});
}
set_commission_claim_permission {
// Create a pool.
let (depositor, pool_account) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into(), Some(CommissionClaimPermission::Account(depositor.clone())))
verify {
assert_eq!(
BondedPools::<T>::get(1).unwrap().commission, Commission {
current: None,
max: None,
change_rate: None,
throttle_from: None,
claim_permission: Some(CommissionClaimPermission::Account(depositor)),
});
}
set_claim_permission {
// Create a pool
let min_create_bond = Pools::<T>::depositor_min_bond();
let (depositor, pool_account) = create_pool_account::<T>(0, min_create_bond, None);
// Join pool
let min_join_bond = MinJoinBond::<T>::get().max(CurrencyOf::<T>::minimum_balance());
let joiner = create_funded_user_with_balance::<T>("joiner", 0, min_join_bond * 4u32.into());
let joiner_lookup = T::Lookup::unlookup(joiner.clone());
Pools::<T>::join(RuntimeOrigin::Signed(joiner.clone()).into(), min_join_bond, 1)
.unwrap();
// Sanity check join worked
assert_eq!(
T::Staking::active_stake(&pool_account).unwrap(),
min_create_bond + min_join_bond
);
}:_(RuntimeOrigin::Signed(joiner.clone()), ClaimPermission::Permissioned)
verify {
assert_eq!(ClaimPermissions::<T>::get(joiner), ClaimPermission::Permissioned);
}
claim_commission {
let claimer: T::AccountId = account("claimer_member", USER_SEED + 4, 0);
let commission = Perbill::from_percent(50);
let origin_weight = Pools::<T>::depositor_min_bond() * 2u32.into();
let ed = CurrencyOf::<T>::minimum_balance();
let (depositor, pool_account) = create_pool_account::<T>(0, origin_weight, Some(commission));
let reward_account = Pools::<T>::create_reward_account(1);
CurrencyOf::<T>::set_balance(&reward_account, ed + origin_weight);
// member claims a payout to make some commission available.
let _ = Pools::<T>::claim_payout(RuntimeOrigin::Signed(claimer.clone()).into());
// set a claim permission to an account.
let _ = Pools::<T>::set_commission_claim_permission(
RuntimeOrigin::Signed(depositor.clone()).into(),
1u32.into(),
Some(CommissionClaimPermission::Account(claimer))
);
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor.clone()), 1u32.into())
verify {
assert_eq!(
CurrencyOf::<T>::balance(&depositor),
origin_weight + commission * origin_weight
);
assert_eq!(
CurrencyOf::<T>::balance(&reward_account),
ed + commission * origin_weight
);
}
adjust_pool_deposit {
// Create a pool
let (depositor, _) = create_pool_account::<T>(0, Pools::<T>::depositor_min_bond() * 2u32.into(), None);
// Remove ed freeze to create a scenario where the ed deposit needs to be adjusted.
let _ = Pools::<T>::unfreeze_pool_deposit(&Pools::<T>::create_reward_account(1));
assert!(&Pools::<T>::check_ed_imbalance().is_err());
whitelist_account!(depositor);
}:_(RuntimeOrigin::Signed(depositor), 1)
verify {
assert!(&Pools::<T>::check_ed_imbalance().is_ok());
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(),
crate::mock::Runtime
);
}

View File

@ -1,29 +0,0 @@
// This file is part of Substrate.
// 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.
//! Benchmarks for the nomination pools coupled with the staking and bags list pallets.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
pub mod inner;
#[cfg(feature = "runtime-benchmarks")]
pub use inner::*;
#[cfg(all(feature = "runtime-benchmarks", test))]
pub(crate) mod mock;

View File

@ -1,205 +0,0 @@
// This file is part of Substrate.
// 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 crate::VoterBagsListInstance;
use frame_election_provider_support::VoteWeight;
use frame_support::{derive_impl, pallet_prelude::*, parameter_types, traits::ConstU64, PalletId};
use sp_runtime::{
traits::{Convert, IdentityLookup},
BuildStorage, FixedU128, Perbill,
};
type AccountId = u128;
type Nonce = u32;
type BlockNumber = u64;
type Balance = u128;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type Nonce = Nonce;
type RuntimeCall = RuntimeCall;
type Hash = sp_core::H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
impl pallet_timestamp::Config for Runtime {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ConstU64<5>;
type WeightInfo = ();
}
parameter_types! {
pub const ExistentialDeposit: Balance = 10;
}
impl pallet_balances::Config for Runtime {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
}
pallet_staking_reward_curve::build! {
const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
}
impl pallet_staking::Config for Runtime {
type Currency = Balances;
type CurrencyBalance = Balance;
type UnixTime = pallet_timestamp::Pallet<Self>;
type CurrencyToVote = ();
type RewardRemainder = ();
type RuntimeEvent = RuntimeEvent;
type Slash = ();
type Reward = ();
type SessionsPerEra = ();
type SlashDeferDuration = ();
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BondingDuration = ConstU32<3>;
type SessionInterface = ();
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = ();
type MaxExposurePageSize = ConstU32<64>;
type ElectionProvider =
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = VoterList;
type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxControllersInDeprecationBatch = ConstU32<100>;
type MaxUnlockingChunks = ConstU32<32>;
type HistoryDepth = ConstU32<84>;
type EventListeners = Pools;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
}
parameter_types! {
pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000];
}
impl pallet_bags_list::Config<VoterBagsListInstance> for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type BagThresholds = BagThresholds;
type ScoreProvider = Staking;
type Score = VoteWeight;
}
pub struct BalanceToU256;
impl Convert<Balance, sp_core::U256> for BalanceToU256 {
fn convert(n: Balance) -> sp_core::U256 {
n.into()
}
}
pub struct U256ToBalance;
impl Convert<sp_core::U256, Balance> for U256ToBalance {
fn convert(n: sp_core::U256) -> Balance {
n.try_into().unwrap()
}
}
parameter_types! {
pub static PostUnbondingPoolsWindow: u32 = 10;
pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
pub const MaxPointsToBalance: u8 = 10;
}
impl ghost_nomination_pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance;
type Staking = Staking;
type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow;
type MaxMetadataLen = ConstU32<256>;
type MaxUnbonding = ConstU32<8>;
type PalletId = PoolsPalletId;
type MaxPointsToBalance = MaxPointsToBalance;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
}
impl crate::Config for Runtime {}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub enum Runtime {
System: frame_system,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
Staking: pallet_staking,
VoterList: pallet_bags_list::<Instance1>,
Pools: ghost_nomination_pools,
}
);
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut storage = frame_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
let _ = ghost_nomination_pools::GenesisConfig::<Runtime> {
min_join_bond: 2,
min_create_bond: 2,
max_pools: Some(3),
max_members_per_pool: Some(3),
max_members: Some(3 * 3),
global_max_commission: Some(Perbill::from_percent(50)),
}
.assimilate_storage(&mut storage);
sp_io::TestExternalities::from(storage)
}

View File

@ -1,30 +0,0 @@
[package]
name = "ghost-nomination-pools-fuzzer"
version = "2.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "Fork of Fuzzer for fixed point arithmetic primitives."
documentation = "https://docs.rs/sp-arithmetic-fuzzer"
publish = false
[dependencies]
honggfuzz = "0.5.54"
ghost-nomination-pools = { workspace = true, features = ["fuzzing"] }
frame-system = { workspace = true }
frame-support = { workspace = true }
sp-runtime = { workspace = true }
sp-io = { workspace = true }
sp-tracing = { workspace = true }
rand = { workspace = true, features = ["small_rng"] }
log = { workspace = true, default-features = true }
[[bin]]
name = "call"
path = "src/call.rs"

View File

@ -1,354 +0,0 @@
// This file is part of Substrate.
// 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.
//! # Running
//! Running this fuzzer can be done with `cargo hfuzz run call`. `honggfuzz` CLI
//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
//!
//! # Debugging a panic
//! Once a panic is found, it can be debugged with
//! `cargo hfuzz run-debug per_thing_rational hfuzz_workspace/call/*.fuzz`.
use frame_support::{
assert_ok,
traits::{Currency, GetCallName, UnfilteredDispatchable},
};
use honggfuzz::fuzz;
use pallet_nomination_pools::{
log,
mock::*,
pallet as pools,
pallet::{BondedPools, Call as PoolsCall, Event as PoolsEvents, PoolMembers},
BondExtra, BondedPool, GlobalMaxCommission, LastPoolId, MaxPoolMembers, MaxPoolMembersPerPool,
MaxPools, MinCreateBond, MinJoinBond, PoolId,
};
use rand::{seq::SliceRandom, Rng};
use sp_runtime::{assert_eq_error_rate, Perbill, Perquintill};
const ERA: BlockNumber = 1000;
const MAX_ED_MULTIPLE: Balance = 10_000;
const MIN_ED_MULTIPLE: Balance = 10;
// not quite elegant, just to make it available in random_signed_origin.
const REWARD_AGENT_ACCOUNT: AccountId = 42;
/// Grab random accounts, either known ones, or new ones.
fn random_signed_origin<R: Rng>(rng: &mut R) -> (RuntimeOrigin, AccountId) {
let count = PoolMembers::<T>::count();
if rng.gen::<bool>() && count > 0 {
// take an existing account.
let skip = rng.gen_range(0..count as usize);
// this is tricky: the account might be our reward agent, which we never want to be
// randomly chosen here. Try another one, or, if it is only our agent, return a random
// one nonetheless.
let candidate = PoolMembers::<T>::iter_keys().skip(skip).take(1).next().unwrap();
let acc =
if candidate == REWARD_AGENT_ACCOUNT { rng.gen::<AccountId>() } else { candidate };
(RuntimeOrigin::signed(acc), acc)
} else {
// create a new account
let acc = rng.gen::<AccountId>();
(RuntimeOrigin::signed(acc), acc)
}
}
fn random_ed_multiple<R: Rng>(rng: &mut R) -> Balance {
let multiple = rng.gen_range(MIN_ED_MULTIPLE..MAX_ED_MULTIPLE);
ExistentialDeposit::get() * multiple
}
fn fund_account<R: Rng>(rng: &mut R, account: &AccountId) {
let target_amount = random_ed_multiple(rng);
if let Some(top_up) = target_amount.checked_sub(Balances::free_balance(account)) {
let _ = Balances::deposit_creating(account, top_up);
}
assert!(Balances::free_balance(account) >= target_amount);
}
fn random_existing_pool<R: Rng>(mut rng: &mut R) -> Option<PoolId> {
BondedPools::<T>::iter_keys().collect::<Vec<_>>().choose(&mut rng).map(|x| *x)
}
fn random_call<R: Rng>(mut rng: &mut R) -> (pools::Call<T>, RuntimeOrigin) {
let op = rng.gen::<usize>();
let mut op_count = <pools::Call<T> as GetCallName>::get_call_names().len();
// Exclude set_state, set_metadata, set_configs, update_roles and chill.
op_count -= 5;
match op % op_count {
0 => {
// join
let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
let (origin, who) = random_signed_origin(&mut rng);
fund_account(&mut rng, &who);
let amount = random_ed_multiple(&mut rng);
(PoolsCall::<T>::join { amount, pool_id }, origin)
},
1 => {
// bond_extra
let (origin, who) = random_signed_origin(&mut rng);
let extra = if rng.gen::<bool>() {
BondExtra::Rewards
} else {
fund_account(&mut rng, &who);
let amount = random_ed_multiple(&mut rng);
BondExtra::FreeBalance(amount)
};
(PoolsCall::<T>::bond_extra { extra }, origin)
},
2 => {
// claim_payout
let (origin, _) = random_signed_origin(&mut rng);
(PoolsCall::<T>::claim_payout {}, origin)
},
3 => {
// unbond
let (origin, who) = random_signed_origin(&mut rng);
let amount = random_ed_multiple(&mut rng);
(PoolsCall::<T>::unbond { member_account: who, unbonding_points: amount }, origin)
},
4 => {
// pool_withdraw_unbonded
let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
let (origin, _) = random_signed_origin(&mut rng);
(PoolsCall::<T>::pool_withdraw_unbonded { pool_id, num_slashing_spans: 0 }, origin)
},
5 => {
// withdraw_unbonded
let (origin, who) = random_signed_origin(&mut rng);
(
PoolsCall::<T>::withdraw_unbonded { member_account: who, num_slashing_spans: 0 },
origin,
)
},
6 => {
// create
let (origin, who) = random_signed_origin(&mut rng);
let amount = random_ed_multiple(&mut rng);
fund_account(&mut rng, &who);
let root = who;
let bouncer = who;
let nominator = who;
(PoolsCall::<T>::create { amount, root, bouncer, nominator }, origin)
},
7 => {
// nominate
let (origin, _) = random_signed_origin(&mut rng);
let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
let validators = Default::default();
(PoolsCall::<T>::nominate { pool_id, validators }, origin)
},
_ => unreachable!(),
}
}
#[derive(Default)]
struct RewardAgent {
who: AccountId,
pool_id: Option<PoolId>,
expected_reward: Balance,
}
// TODO: inject some slashes into the game.
impl RewardAgent {
fn new(who: AccountId) -> Self {
Self { who, ..Default::default() }
}
fn join(&mut self) {
if self.pool_id.is_some() {
return
}
let pool_id = LastPoolId::<T>::get();
let amount = 10 * ExistentialDeposit::get();
let origin = RuntimeOrigin::signed(self.who);
let _ = Balances::deposit_creating(&self.who, 10 * amount);
self.pool_id = Some(pool_id);
log::info!(target: "reward-agent", "🤖 reward agent joining in {} with {}", pool_id, amount);
assert_ok!(PoolsCall::join::<T> { amount, pool_id }.dispatch_bypass_filter(origin));
}
fn claim_payout(&mut self) {
// 10 era later, we claim our payout. We expect our income to be roughly what we
// calculated.
if !PoolMembers::<T>::contains_key(&self.who) {
log!(warn, "reward agent is not in the pool yet, cannot claim");
return
}
let pre = Balances::free_balance(&42);
let origin = RuntimeOrigin::signed(42);
assert_ok!(PoolsCall::<T>::claim_payout {}.dispatch_bypass_filter(origin));
let post = Balances::free_balance(&42);
let income = post - pre;
log::info!(
target: "reward-agent", "🤖 CLAIM: actual: {}, expected: {}",
income,
self.expected_reward,
);
assert_eq_error_rate!(income, self.expected_reward, 10);
self.expected_reward = 0;
}
}
fn main() {
let mut reward_agent = RewardAgent::new(REWARD_AGENT_ACCOUNT);
sp_tracing::try_init_simple();
let mut ext = sp_io::TestExternalities::new_empty();
let mut events_histogram = Vec::<(PoolsEvents<T>, u32)>::default();
let mut iteration = 0 as BlockNumber;
let mut ok = 0;
let mut err = 0;
let dot: Balance = (10 as Balance).pow(10);
ExistentialDeposit::set(dot);
BondingDuration::set(8);
ext.execute_with(|| {
MaxPoolMembers::<T>::set(Some(10_000));
MaxPoolMembersPerPool::<T>::set(Some(1000));
MaxPools::<T>::set(Some(1_000));
GlobalMaxCommission::<T>::set(Some(Perbill::from_percent(25)));
MinCreateBond::<T>::set(10 * ExistentialDeposit::get());
MinJoinBond::<T>::set(5 * ExistentialDeposit::get());
System::set_block_number(1);
});
loop {
fuzz!(|seed: [u8; 32]| {
use ::rand::{rngs::SmallRng, SeedableRng};
let mut rng = SmallRng::from_seed(seed);
ext.execute_with(|| {
let (call, origin) = random_call(&mut rng);
let outcome = call.clone().dispatch_bypass_filter(origin.clone());
iteration += 1;
match outcome {
Ok(_) => ok += 1,
Err(_) => err += 1,
};
log!(
trace,
"iteration {}, call {:?}, origin {:?}, outcome: {:?}, so far {} ok {} err",
iteration,
call,
origin,
outcome,
ok,
err,
);
// possibly join the reward_agent
if iteration > ERA / 2 && BondedPools::<T>::count() > 0 {
reward_agent.join();
}
// and possibly roughly every 4 era, trigger payout for the agent. Doing this more
// frequent is also harmless.
if rng.gen_range(0..(4 * ERA)) == 0 {
reward_agent.claim_payout();
}
// execute sanity checks at a fixed interval, possibly on every block.
if iteration %
(std::env::var("SANITY_CHECK_INTERVAL")
.ok()
.and_then(|x| x.parse::<u64>().ok()))
.unwrap_or(1) == 0
{
log!(info, "running sanity checks at {}", iteration);
Pools::do_try_state(u8::MAX).unwrap();
}
// collect and reset events.
System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| {
if let pallet_nomination_pools::mock::RuntimeEvent::Pools(inner) = e {
Some(inner)
} else {
None
}
})
.for_each(|e| {
if let Some((_, c)) = events_histogram
.iter_mut()
.find(|(x, _)| std::mem::discriminant(x) == std::mem::discriminant(&e))
{
*c += 1;
} else {
events_histogram.push((e, 1))
}
});
System::reset_events();
// trigger an era change, and check the status of the reward agent.
if iteration % ERA == 0 {
CurrentEra::mutate(|c| *c += 1);
BondedPools::<T>::iter().for_each(|(id, _)| {
let amount = random_ed_multiple(&mut rng);
let _ =
Balances::deposit_creating(&Pools::create_reward_account(id), amount);
// if we just paid out the reward agent, let's calculate how much we expect
// our reward agent to have earned.
if reward_agent.pool_id.map_or(false, |mid| mid == id) {
let all_points = BondedPool::<T>::get(id).map(|p| p.points).unwrap();
let member_points =
PoolMembers::<T>::get(reward_agent.who).map(|m| m.points).unwrap();
let agent_share = Perquintill::from_rational(member_points, all_points);
log::info!(
target: "reward-agent",
"🤖 REWARD = amount = {:?}, ratio: {:?}, share {:?}",
amount,
agent_share,
agent_share * amount,
);
reward_agent.expected_reward += agent_share * amount;
}
});
log!(
info,
"iteration {}, {} pools, {} members, {} ok {} err, events = {:?}",
iteration,
BondedPools::<T>::count(),
PoolMembers::<T>::count(),
ok,
err,
events_histogram
.iter()
.map(|(x, c)| (
format!("{:?}", x)
.split(" ")
.map(|x| x.to_string())
.collect::<Vec<_>>()
.first()
.cloned()
.unwrap(),
c,
))
.collect::<Vec<_>>(),
);
}
})
})
}
}

View File

@ -1,20 +0,0 @@
[package]
name = "ghost-nomination-pools-runtime-api"
version = "23.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "Fork of Runtime API for nomination-pools FRAME pallet"
readme = "README.md"
[dependencies]
codec = { workspace = true, default-features = false, features = ["derive"] }
sp-api = { workspace = true, default-features = false }
sp-std = { workspace = true, default-features = false }
ghost-nomination-pools = { workspace = true, default-features = false }
[features]
default = ["std"]
std = ["codec/std", "ghost-nomination-pools/std", "sp-api/std", "sp-std/std"]

View File

@ -1,3 +0,0 @@
Runtime API definition for nomination-pools pallet.
License: Apache-2.0

View File

@ -1,42 +0,0 @@
// This file is part of Substrate.
// 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.
//! Runtime API definition for nomination-pools pallet.
//! Currently supports only one rpc endpoint.
#![cfg_attr(not(feature = "std"), no_std)]
use codec::Codec;
use ghost_nomination_pools::PoolId;
sp_api::decl_runtime_apis! {
/// Runtime api for accessing information about nomination pools.
pub trait NominationPoolsApi<AccountId, Balance>
where
AccountId: Codec,
Balance: Codec,
{
/// Returns the pending rewards for the member that the AccountId was given for.
fn pending_rewards(who: AccountId) -> Balance;
/// Returns the equivalent balance of `points` for a given pool.
fn points_to_balance(pool_id: PoolId, points: Balance) -> Balance;
/// Returns the equivalent points of `new_funds` for a given pool.
fn balance_to_points(pool_id: PoolId, new_funds: Balance) -> Balance;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,574 +0,0 @@
// This file is part of Substrate.
// 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 crate::{self as pools};
use frame_support::{
assert_ok, derive_impl, ord_parameter_types, parameter_types, traits::fungible::Mutate,
PalletId,
};
use frame_system::{EnsureSignedBy, RawOrigin};
use sp_runtime::{BuildStorage, FixedU128};
use sp_staking::{OnStakingUpdate, Stake};
pub type BlockNumber = u64;
pub type AccountId = u128;
pub type Balance = u128;
pub type RewardCounter = FixedU128;
// This sneaky little hack allows us to write code exactly as we would do in the pallet in the tests
// as well, e.g. `StorageItem::<T>::get()`.
pub type T = Runtime;
pub type Currency = <T as Config>::Currency;
// Ext builder creates a pool with id 1.
pub fn default_bonded_account() -> AccountId {
Pools::create_bonded_account(1)
}
// Ext builder creates a pool with id 1.
pub fn default_reward_account() -> AccountId {
Pools::create_reward_account(1)
}
parameter_types! {
pub static MinJoinBondConfig: Balance = 2;
pub static CurrentEra: EraIndex = 0;
pub static BondingDuration: EraIndex = 3;
pub storage BondedBalanceMap: BTreeMap<AccountId, Balance> = Default::default();
// map from a user to a vec of eras and amounts being unlocked in each era.
pub storage UnbondingBalanceMap: BTreeMap<AccountId, Vec<(EraIndex, Balance)>> = Default::default();
#[derive(Clone, PartialEq)]
pub static MaxUnbonding: u32 = 8;
pub static StakingMinBond: Balance = 10;
pub storage Nominations: Option<Vec<AccountId>> = None;
}
pub struct StakingMock;
impl StakingMock {
pub(crate) fn set_bonded_balance(who: AccountId, bonded: Balance) {
let mut x = BondedBalanceMap::get();
x.insert(who, bonded);
BondedBalanceMap::set(&x)
}
/// Mimics a slash towards a pool specified by `pool_id`.
/// This reduces the bonded balance of a pool by `amount` and calls [`Pools::on_slash`] to
/// enact changes in the nomination-pool pallet.
///
/// Does not modify any [`SubPools`] of the pool as [`Default::default`] is passed for
/// `slashed_unlocking`.
pub fn slash_by(pool_id: PoolId, amount: Balance) {
let acc = Pools::create_bonded_account(pool_id);
let bonded = BondedBalanceMap::get();
let pre_total = bonded.get(&acc).unwrap();
Self::set_bonded_balance(acc, pre_total - amount);
Pools::on_slash(&acc, pre_total - amount, &Default::default(), amount);
}
}
impl sp_staking::StakingInterface for StakingMock {
type Balance = Balance;
type AccountId = AccountId;
type CurrencyToVote = ();
fn minimum_nominator_bond() -> Self::Balance {
StakingMinBond::get()
}
fn minimum_validator_bond() -> Self::Balance {
StakingMinBond::get()
}
fn desired_validator_count() -> u32 {
unimplemented!("method currently not used in testing")
}
fn current_era() -> EraIndex {
CurrentEra::get()
}
fn bonding_duration() -> EraIndex {
BondingDuration::get()
}
fn status(
_: &Self::AccountId,
) -> Result<sp_staking::StakerStatus<Self::AccountId>, DispatchError> {
Nominations::get()
.map(|noms| sp_staking::StakerStatus::Nominator(noms))
.ok_or(DispatchError::Other("NotStash"))
}
fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult {
let mut x = BondedBalanceMap::get();
x.get_mut(who).map(|v| *v += extra);
BondedBalanceMap::set(&x);
Ok(())
}
fn unbond(who: &Self::AccountId, amount: Self::Balance) -> DispatchResult {
let mut x = BondedBalanceMap::get();
*x.get_mut(who).unwrap() = x.get_mut(who).unwrap().saturating_sub(amount);
BondedBalanceMap::set(&x);
let era = Self::current_era();
let unlocking_at = era + Self::bonding_duration();
let mut y = UnbondingBalanceMap::get();
y.entry(*who).or_insert(Default::default()).push((unlocking_at, amount));
UnbondingBalanceMap::set(&y);
Ok(())
}
fn update_payee(_stash: &Self::AccountId, _reward_acc: &Self::AccountId) -> DispatchResult {
unimplemented!("method currently not used in testing")
}
fn chill(_: &Self::AccountId) -> sp_runtime::DispatchResult {
Ok(())
}
fn withdraw_unbonded(who: Self::AccountId, _: u32) -> Result<bool, DispatchError> {
let mut unbonding_map = UnbondingBalanceMap::get();
// closure to calculate the current unlocking funds across all eras/accounts.
let unlocking = |pair: &Vec<(EraIndex, Balance)>| -> Balance {
pair.iter()
.try_fold(Zero::zero(), |acc: Balance, (_at, amount)| acc.checked_add(*amount))
.unwrap()
};
let staker_map = unbonding_map.get_mut(&who).ok_or("Nothing to unbond")?;
let unlocking_before = unlocking(&staker_map);
let current_era = Self::current_era();
staker_map.retain(|(unlocking_at, _amount)| *unlocking_at > current_era);
// if there was a withdrawal, notify the pallet.
Pools::on_withdraw(&who, unlocking_before.saturating_sub(unlocking(&staker_map)));
UnbondingBalanceMap::set(&unbonding_map);
Ok(UnbondingBalanceMap::get().is_empty() && BondedBalanceMap::get().is_empty())
}
fn bond(stash: &Self::AccountId, value: Self::Balance, _: &Self::AccountId) -> DispatchResult {
StakingMock::set_bonded_balance(*stash, value);
Ok(())
}
fn nominate(_: &Self::AccountId, nominations: Vec<Self::AccountId>) -> DispatchResult {
Nominations::set(&Some(nominations));
Ok(())
}
#[cfg(feature = "runtime-benchmarks")]
fn nominations(_: &Self::AccountId) -> Option<Vec<Self::AccountId>> {
Nominations::get()
}
fn stash_by_ctrl(_controller: &Self::AccountId) -> Result<Self::AccountId, DispatchError> {
unimplemented!("method currently not used in testing")
}
fn stake(who: &Self::AccountId) -> Result<Stake<Balance>, DispatchError> {
match (UnbondingBalanceMap::get().get(who), BondedBalanceMap::get().get(who).copied()) {
(None, None) => Err(DispatchError::Other("balance not found")),
(Some(v), None) => Ok(Stake {
total: v.into_iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)),
active: 0,
}),
(None, Some(v)) => Ok(Stake { total: v, active: v }),
(Some(a), Some(b)) => Ok(Stake {
total: a.into_iter().fold(0u128, |acc, &x| acc.saturating_add(x.1)) + b,
active: b,
}),
}
}
fn election_ongoing() -> bool {
unimplemented!("method currently not used in testing")
}
fn force_unstake(_who: Self::AccountId) -> sp_runtime::DispatchResult {
unimplemented!("method currently not used in testing")
}
fn is_exposed_in_era(_who: &Self::AccountId, _era: &EraIndex) -> bool {
unimplemented!("method currently not used in testing")
}
#[cfg(feature = "runtime-benchmarks")]
fn add_era_stakers(
_current_era: &EraIndex,
_stash: &Self::AccountId,
_exposures: Vec<(Self::AccountId, Self::Balance)>,
) {
unimplemented!("method currently not used in testing")
}
#[cfg(feature = "runtime-benchmarks")]
fn set_current_era(_era: EraIndex) {
unimplemented!("method currently not used in testing")
}
#[cfg(feature = "runtime-benchmarks")]
fn max_exposure_page_size() -> sp_staking::Page {
unimplemented!("method currently not used in testing")
}
fn slash_reward_fraction() -> Perbill {
unimplemented!("method currently not used in testing")
}
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type SS58Prefix = ();
type BaseCallFilter = frame_support::traits::Everything;
type RuntimeOrigin = RuntimeOrigin;
type Nonce = u64;
type RuntimeCall = RuntimeCall;
type Hash = sp_core::H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = sp_runtime::traits::IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ();
type DbWeight = ();
type BlockLength = ();
type BlockWeights = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
parameter_types! {
pub static ExistentialDeposit: Balance = 5;
}
impl pallet_balances::Config for Runtime {
type MaxLocks = frame_support::traits::ConstU32<1024>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
}
pub struct BalanceToU256;
impl Convert<Balance, U256> for BalanceToU256 {
fn convert(n: Balance) -> U256 {
n.into()
}
}
pub struct U256ToBalance;
impl Convert<U256, Balance> for U256ToBalance {
fn convert(n: U256) -> Balance {
n.try_into().unwrap()
}
}
parameter_types! {
pub static PostUnbondingPoolsWindow: u32 = 2;
pub static MaxMetadataLen: u32 = 2;
pub static CheckLevel: u8 = 255;
pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
}
ord_parameter_types! {
pub const Admin: u128 = 42;
}
impl pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = RewardCounter;
type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance;
type Staking = StakingMock;
type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow;
type PalletId = PoolsPalletId;
type MaxMetadataLen = MaxMetadataLen;
type MaxUnbonding = MaxUnbonding;
type MaxPointsToBalance = frame_support::traits::ConstU8<10>;
type AdminOrigin = EnsureSignedBy<Admin, AccountId>;
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub enum Runtime {
System: frame_system,
Balances: pallet_balances,
Pools: pools,
}
);
pub struct ExtBuilder {
members: Vec<(AccountId, Balance)>,
max_members: Option<u32>,
max_members_per_pool: Option<u32>,
global_max_commission: Option<Perbill>,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
members: Default::default(),
max_members: Some(4),
max_members_per_pool: Some(3),
global_max_commission: Some(Perbill::from_percent(90)),
}
}
}
#[cfg_attr(feature = "fuzzing", allow(dead_code))]
impl ExtBuilder {
// Add members to pool 0.
pub fn add_members(mut self, members: Vec<(AccountId, Balance)>) -> Self {
self.members = members;
self
}
pub fn ed(self, ed: Balance) -> Self {
ExistentialDeposit::set(ed);
self
}
pub fn min_bond(self, min: Balance) -> Self {
StakingMinBond::set(min);
self
}
pub fn min_join_bond(self, min: Balance) -> Self {
MinJoinBondConfig::set(min);
self
}
pub fn with_check(self, level: u8) -> Self {
CheckLevel::set(level);
self
}
pub fn max_members(mut self, max: Option<u32>) -> Self {
self.max_members = max;
self
}
pub fn max_members_per_pool(mut self, max: Option<u32>) -> Self {
self.max_members_per_pool = max;
self
}
pub fn global_max_commission(mut self, commission: Option<Perbill>) -> Self {
self.global_max_commission = commission;
self
}
pub fn build(self) -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut storage =
frame_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
let _ = crate::GenesisConfig::<Runtime> {
min_join_bond: MinJoinBondConfig::get(),
min_create_bond: 2,
max_pools: Some(2),
max_members_per_pool: self.max_members_per_pool,
max_members: self.max_members,
global_max_commission: self.global_max_commission,
}
.assimilate_storage(&mut storage);
let mut ext = sp_io::TestExternalities::from(storage);
ext.execute_with(|| {
// for events to be deposited.
frame_system::Pallet::<Runtime>::set_block_number(1);
// make a pool
let amount_to_bond = Pools::depositor_min_bond();
Currency::set_balance(&10, amount_to_bond * 5);
assert_ok!(Pools::create(RawOrigin::Signed(10).into(), amount_to_bond, 900, 901, 902));
assert_ok!(Pools::set_metadata(RuntimeOrigin::signed(900), 1, vec![1, 1]));
let last_pool = LastPoolId::<Runtime>::get();
for (account_id, bonded) in self.members {
<Runtime as Config>::Currency::set_balance(&account_id, bonded * 2);
assert_ok!(Pools::join(RawOrigin::Signed(account_id).into(), bonded, last_pool));
}
});
ext
}
pub fn build_and_execute(self, test: impl FnOnce()) {
self.build().execute_with(|| {
test();
Pools::do_try_state(CheckLevel::get()).unwrap();
})
}
}
pub fn unsafe_set_state(pool_id: PoolId, state: PoolState) {
BondedPools::<Runtime>::try_mutate(pool_id, |maybe_bonded_pool| {
maybe_bonded_pool.as_mut().ok_or(()).map(|bonded_pool| {
bonded_pool.state = state;
})
})
.unwrap()
}
parameter_types! {
storage PoolsEvents: u32 = 0;
storage BalancesEvents: u32 = 0;
}
/// Helper to run a specified amount of blocks.
pub fn run_blocks(n: u64) {
let current_block = System::block_number();
run_to_block(n + current_block);
}
/// Helper to run to a specific block.
pub fn run_to_block(n: u64) {
let current_block = System::block_number();
assert!(n > current_block);
while System::block_number() < n {
Pools::on_finalize(System::block_number());
System::set_block_number(System::block_number() + 1);
Pools::on_initialize(System::block_number());
}
}
/// All events of this pallet.
pub fn pool_events_since_last_call() -> Vec<super::Event<Runtime>> {
let events = System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = PoolsEvents::get();
PoolsEvents::set(&(events.len() as u32));
events.into_iter().skip(already_seen as usize).collect()
}
/// All events of the `Balances` pallet.
pub fn balances_events_since_last_call() -> Vec<pallet_balances::Event<Runtime>> {
let events = System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Balances(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = BalancesEvents::get();
BalancesEvents::set(&(events.len() as u32));
events.into_iter().skip(already_seen as usize).collect()
}
/// Same as `fully_unbond`, in permissioned setting.
pub fn fully_unbond_permissioned(member: AccountId) -> DispatchResult {
let points = PoolMembers::<Runtime>::get(member)
.map(|d| d.active_points())
.unwrap_or_default();
Pools::unbond(RuntimeOrigin::signed(member), member, points)
}
pub fn pending_rewards_for_delegator(delegator: AccountId) -> Balance {
let member = PoolMembers::<T>::get(delegator).unwrap();
let bonded_pool = BondedPools::<T>::get(member.pool_id).unwrap();
let reward_pool = RewardPools::<T>::get(member.pool_id).unwrap();
assert!(!bonded_pool.points.is_zero());
let commission = bonded_pool.commission.current();
let current_rc = reward_pool
.current_reward_counter(member.pool_id, bonded_pool.points, commission)
.unwrap()
.0;
member.pending_rewards(current_rc).unwrap_or_default()
}
#[derive(PartialEq, Debug)]
pub enum RewardImbalance {
// There is no reward deficit.
Surplus(Balance),
// There is a reward deficit.
Deficit(Balance),
}
pub fn pool_pending_rewards(pool: PoolId) -> Result<BalanceOf<T>, sp_runtime::DispatchError> {
let bonded_pool = BondedPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
let reward_pool = RewardPools::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
let current_rc = if !bonded_pool.points.is_zero() {
let commission = bonded_pool.commission.current();
reward_pool.current_reward_counter(pool, bonded_pool.points, commission)?.0
} else {
Default::default()
};
Ok(PoolMembers::<T>::iter()
.filter(|(_, d)| d.pool_id == pool)
.map(|(_, d)| d.pending_rewards(current_rc).unwrap_or_default())
.fold(0u32.into(), |acc: BalanceOf<T>, x| acc.saturating_add(x)))
}
pub fn reward_imbalance(pool: PoolId) -> RewardImbalance {
let pending_rewards = pool_pending_rewards(pool).expect("pool should exist");
let current_balance = RewardPool::<Runtime>::current_balance(pool);
if pending_rewards > current_balance {
RewardImbalance::Deficit(pending_rewards - current_balance)
} else {
RewardImbalance::Surplus(current_balance - pending_rewards)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn u256_to_balance_convert_works() {
assert_eq!(U256ToBalance::convert(0u32.into()), Zero::zero());
assert_eq!(U256ToBalance::convert(Balance::max_value().into()), Balance::max_value())
}
#[test]
#[should_panic]
fn u256_to_balance_convert_panics_correctly() {
U256ToBalance::convert(U256::from(Balance::max_value()).saturating_add(1u32.into()));
}
#[test]
fn balance_to_u256_convert_works() {
assert_eq!(BalanceToU256::convert(0u32.into()), U256::zero());
assert_eq!(BalanceToU256::convert(Balance::max_value()), Balance::max_value().into())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
[package]
name = "pallet-nomination-pools-test-staking"
version = "1.0.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage = "https://substrate.io"
repository.workspace = true
description = "FRAME nomination pools pallet tests with the staking pallet"
publish = false
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dev-dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] }
scale-info = { version = "2.11.1", features = ["derive"] }
sp-runtime = { path = "../../../primitives/runtime" }
sp-io = { path = "../../../primitives/io" }
sp-std = { path = "../../../primitives/std" }
sp-staking = { path = "../../../primitives/staking" }
sp-core = { path = "../../../primitives/core" }
frame-system = { path = "../../system" }
frame-support = { path = "../../support" }
frame-election-provider-support = { path = "../../election-provider-support" }
pallet-timestamp = { path = "../../timestamp" }
pallet-balances = { path = "../../balances" }
pallet-staking = { path = "../../staking" }
pallet-bags-list = { path = "../../bags-list" }
pallet-staking-reward-curve = { path = "../../staking/reward-curve" }
pallet-nomination-pools = { path = ".." }
sp-tracing = { path = "../../../primitives/tracing" }
log = { workspace = true, default-features = true }

View File

@ -1,823 +0,0 @@
// This file is part of Substrate.
// 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.
#![cfg(test)]
mod mock;
use frame_support::{assert_noop, assert_ok, traits::Currency};
use mock::*;
use pallet_nomination_pools::{
BondExtra, BondedPools, Error as PoolsError, Event as PoolsEvent, LastPoolId, PoolMember,
PoolMembers, PoolState,
};
use pallet_staking::{
CurrentEra, Error as StakingError, Event as StakingEvent, Payee, RewardDestination,
};
use sp_runtime::{bounded_btree_map, traits::Zero};
#[test]
fn pool_lifecycle_e2e() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::minimum_balance(), 5);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 50, 10, 10, 10));
assert_eq!(LastPoolId::<Runtime>::get(), 1);
// have the pool nominate.
assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 50 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 50, joined: true },
]
);
// have two members join
assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1));
assert_ok!(Pools::join(RuntimeOrigin::signed(21), 10, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true },
PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 10, joined: true },
]
);
// pool goes into destroying
assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying));
// depositor cannot unbond yet.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 50),
PoolsError::<Runtime>::MinimumBondNotMet,
);
// now the members want to unbond.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10));
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras.len(), 1);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 0);
assert_eq!(PoolMembers::<Runtime>::get(21).unwrap().unbonding_eras.len(), 1);
assert_eq!(PoolMembers::<Runtime>::get(21).unwrap().points, 0);
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying },
PoolsEvent::Unbonded { member: 20, pool_id: 1, points: 10, balance: 10, era: 3 },
PoolsEvent::Unbonded { member: 21, pool_id: 1, points: 10, balance: 10, era: 3 },
]
);
// depositor cannot still unbond
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 50),
PoolsError::<Runtime>::MinimumBondNotMet,
);
for e in 1..BondingDuration::get() {
CurrentEra::<Runtime>::set(Some(e));
assert_noop!(
Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0),
PoolsError::<Runtime>::CannotWithdrawAny
);
}
// members are now unlocked.
CurrentEra::<Runtime>::set(Some(BondingDuration::get()));
// depositor cannot still unbond
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 50),
PoolsError::<Runtime>::MinimumBondNotMet,
);
// but members can now withdraw.
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0));
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0));
assert!(PoolMembers::<Runtime>::get(20).is_none());
assert!(PoolMembers::<Runtime>::get(21).is_none());
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 20 },]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Withdrawn { member: 20, pool_id: 1, points: 10, balance: 10 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 20 },
PoolsEvent::Withdrawn { member: 21, pool_id: 1, points: 10, balance: 10 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 21 },
]
);
// as soon as all members have left, the depositor can try to unbond, but since the
// min-nominator intention is set, they must chill first.
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(10), 10, 50),
pallet_staking::Error::<Runtime>::InsufficientBond
);
assert_ok!(Pools::chill(RuntimeOrigin::signed(10), 1));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 50));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Chilled { stash: POOL1_BONDED },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 50 },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 50, balance: 50, era: 6 }]
);
// waiting another bonding duration:
CurrentEra::<Runtime>::set(Some(BondingDuration::get() * 2));
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 1));
// pools is fully destroyed now.
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 50 },]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Withdrawn { member: 10, pool_id: 1, points: 50, balance: 50 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 10 },
PoolsEvent::Destroyed { pool_id: 1 }
]
);
})
}
#[test]
fn pool_chill_e2e() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::minimum_balance(), 5);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 50, 10, 10, 10));
assert_eq!(LastPoolId::<Runtime>::get(), 1);
// have the pool nominate.
assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 50 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 50, joined: true },
]
);
// have two members join
assert_ok!(Pools::join(RuntimeOrigin::signed(20), 10, 1));
assert_ok!(Pools::join(RuntimeOrigin::signed(21), 10, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 10, joined: true },
PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 10, joined: true },
]
);
// in case depositor does not have more than `MinNominatorBond` staked, we can end up in
// situation where a member unbonding would cause pool balance to drop below
// `MinNominatorBond` and hence not allowed. This can happen if the `MinNominatorBond` is
// increased after the pool is created.
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
pallet_staking::ConfigOp::Set(55), // minimum nominator bond
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
));
// members can unbond as long as total stake of the pool is above min nominator bond
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10),);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().unbonding_eras.len(), 1);
assert_eq!(PoolMembers::<Runtime>::get(20).unwrap().points, 0);
// this member cannot unbond since it will cause `pool stake < MinNominatorBond`
assert_noop!(
Pools::unbond(RuntimeOrigin::signed(21), 21, 10),
StakingError::<Runtime>::InsufficientBond,
);
// members can call `chill` permissionlessly now
assert_ok!(Pools::chill(RuntimeOrigin::signed(20), 1));
// now another member can unbond.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10));
assert_eq!(PoolMembers::<Runtime>::get(21).unwrap().unbonding_eras.len(), 1);
assert_eq!(PoolMembers::<Runtime>::get(21).unwrap().points, 0);
// nominator can not resume nomination until depositor have enough stake
assert_noop!(
Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]),
PoolsError::<Runtime>::MinimumBondNotMet,
);
// other members joining pool does not affect the depositor's ability to resume nomination
assert_ok!(Pools::join(RuntimeOrigin::signed(22), 10, 1));
assert_noop!(
Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]),
PoolsError::<Runtime>::MinimumBondNotMet,
);
// depositor can bond extra stake
assert_ok!(Pools::bond_extra(RuntimeOrigin::signed(10), BondExtra::FreeBalance(10)));
// `chill` can not be called permissionlessly anymore
assert_noop!(
Pools::chill(RuntimeOrigin::signed(20), 1),
PoolsError::<Runtime>::NotNominator,
);
// now nominator can resume nomination
assert_ok!(Pools::nominate(RuntimeOrigin::signed(10), 1, vec![1, 2, 3]));
// skip to make the unbonding period end.
CurrentEra::<Runtime>::set(Some(BondingDuration::get()));
// members can now withdraw.
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0));
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Chilled { stash: POOL1_BONDED },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, // other member bonding
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 10 }, // depositor bond extra
StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 20 },
]
);
})
}
#[test]
fn pool_slash_e2e() {
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10));
assert_eq!(LastPoolId::<Runtime>::get(), 1);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);
assert_eq!(
Payee::<Runtime>::get(POOL1_BONDED),
Some(RewardDestination::Account(POOL1_REWARD))
);
// have two members join
assert_ok!(Pools::join(RuntimeOrigin::signed(20), 20, 1));
assert_ok!(Pools::join(RuntimeOrigin::signed(21), 20, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: 20 }
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: 20, joined: true },
PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: 20, joined: true },
]
);
// now let's progress a bit.
CurrentEra::<Runtime>::set(Some(1));
// 20 / 80 of the total funds are unlocked, and safe from any further slash.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 4 },
PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 4 }
]
);
CurrentEra::<Runtime>::set(Some(2));
// note: depositor cannot fully unbond at this point.
// these funds will still get slashed.
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 10));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 10));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Unbonded { member: 10, pool_id: 1, balance: 10, points: 10, era: 5 },
PoolsEvent::Unbonded { member: 20, pool_id: 1, balance: 10, points: 10, era: 5 },
PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 10, points: 10, era: 5 },
]
);
// At this point, 20 are safe from slash, 30 are unlocking but vulnerable to slash, and
// another 30 are active and vulnerable to slash. Let's slash half of them.
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
30,
&mut Default::default(),
&mut Default::default(),
2, // slash era 2, affects chunks at era 5 onwards.
);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
// 30 has been slashed to 15 (15 slash)
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 5, balance: 15 },
// 30 has been slashed to 15 (15 slash)
PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 }
]
);
CurrentEra::<Runtime>::set(Some(3));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, 10));
assert_eq!(
PoolMembers::<Runtime>::get(21).unwrap(),
PoolMember {
pool_id: 1,
points: 0,
last_recorded_reward_counter: Zero::zero(),
// the 10 points unlocked just now correspond to 5 points in the unbond pool.
unbonding_eras: bounded_btree_map!(5 => 10, 6 => 5)
}
);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 5 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded { member: 21, pool_id: 1, balance: 5, points: 5, era: 6 }]
);
// now we start withdrawing. we do it all at once, at era 6 where 20 and 21 are fully free.
CurrentEra::<Runtime>::set(Some(6));
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(20), 20, 0));
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(21), 21, 0));
assert_eq!(
pool_events_since_last_call(),
vec![
// 20 had unbonded 10 safely, and 10 got slashed by half.
PoolsEvent::Withdrawn { member: 20, pool_id: 1, balance: 10 + 5, points: 20 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 20 },
// 21 unbonded all of it after the slash
PoolsEvent::Withdrawn { member: 21, pool_id: 1, balance: 5 + 5, points: 15 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 21 }
]
);
assert_eq!(
staking_events_since_last_call(),
// a 10 (un-slashed) + 10/2 (slashed) balance from 10 has also been unlocked
vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 15 + 10 + 15 }]
);
// now, finally, we can unbond the depositor further than their current limit.
assert_ok!(Pools::set_state(RuntimeOrigin::signed(10), 1, PoolState::Destroying));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 20));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: 10 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::StateChanged { pool_id: 1, new_state: PoolState::Destroying },
PoolsEvent::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 }
]
);
CurrentEra::<Runtime>::set(Some(9));
assert_eq!(
PoolMembers::<Runtime>::get(10).unwrap(),
PoolMember {
pool_id: 1,
points: 0,
last_recorded_reward_counter: Zero::zero(),
unbonding_eras: bounded_btree_map!(4 => 10, 5 => 10, 9 => 10)
}
);
// withdraw the depositor, they should lose 12 balance in total due to slash.
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 10, 0));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Withdrawn { stash: POOL1_BONDED, amount: 10 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Withdrawn { member: 10, pool_id: 1, balance: 10 + 15, points: 30 },
PoolsEvent::MemberRemoved { pool_id: 1, member: 10 },
PoolsEvent::Destroyed { pool_id: 1 }
]
);
});
}
#[test]
fn pool_slash_proportional() {
// a typical example where 3 pool members unbond in era 99, 100, and 101, and a slash that
// happened in era 100 should only affect the latter two.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10));
assert_eq!(LastPoolId::<T>::get(), 1);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);
// have two members join
let bond = 20;
assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1));
assert_ok!(Pools::join(RuntimeOrigin::signed(21), bond, 1));
assert_ok!(Pools::join(RuntimeOrigin::signed(22), bond, 1));
assert_eq!(
staking_events_since_last_call(),
vec![
StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond },
StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond },
]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true },
PoolsEvent::Bonded { member: 21, pool_id: 1, bonded: bond, joined: true },
PoolsEvent::Bonded { member: 22, pool_id: 1, bonded: bond, joined: true },
]
);
// now let's progress a lot.
CurrentEra::<T>::set(Some(99));
// and unbond
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded {
member: 20,
pool_id: 1,
balance: bond,
points: bond,
era: 127
}]
);
CurrentEra::<T>::set(Some(100));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(21), 21, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded {
member: 21,
pool_id: 1,
balance: bond,
points: bond,
era: 128
}]
);
CurrentEra::<T>::set(Some(101));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(22), 22, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond },]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded {
member: 22,
pool_id: 1,
balance: bond,
points: bond,
era: 129
}]
);
// Apply a slash that happened in era 100. This is typically applied with a delay.
// Of the total 100, 50 is slashed.
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
50,
&mut Default::default(),
&mut Default::default(),
100,
);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
// This era got slashed 12.5, which rounded up to 13.
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 128, balance: 7 },
// This era got slashed 12 instead of 12.5 because an earlier chunk got 0.5 more
// slashed, and 12 is all the remaining slash
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 129, balance: 8 },
// Bonded pool got slashed for 25, remaining 15 in it.
PoolsEvent::PoolSlashed { pool_id: 1, balance: 15 }
]
);
});
}
#[test]
fn pool_slash_non_proportional_only_bonded_pool() {
// A typical example where a pool member unbonds in era 99, and they can get away with a slash
// that happened in era 100, as long as the pool has enough active bond to cover the slash. If
// everything else in the slashing/staking system works, this should always be the case.
// Nonetheless, `ledger.slash` has been written such that it will slash greedily from any chunk
// if it runs out of chunks that it thinks should be affected by the slash.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);
// have two members join
let bond = 20;
assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }]
);
// progress and unbond.
CurrentEra::<T>::set(Some(99));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded {
member: 20,
pool_id: 1,
balance: bond,
points: bond,
era: 127
}]
);
// slash for 30. This will be deducted only from the bonded pool.
CurrentEra::<T>::set(Some(100));
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
30,
&mut Default::default(),
&mut Default::default(),
100,
);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 30 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::PoolSlashed { pool_id: 1, balance: 10 }]
);
});
}
#[test]
fn pool_slash_non_proportional_bonded_pool_and_chunks() {
// An uncommon example where even though some funds are unlocked such that they should not be
// affected by a slash, we still slash out of them. This should not happen at all. If a
// nomination has unbonded, from the next era onwards, their exposure will drop, so if an era
// happens in that era, then their share of that slash should naturally be less, such that only
// their active ledger stake is enough to compensate it.
new_test_ext().execute_with(|| {
ExistentialDeposit::set(1);
BondingDuration::set(28);
assert_eq!(Balances::minimum_balance(), 1);
assert_eq!(Staking::current_era(), None);
// create the pool, we know this has id 1.
assert_ok!(Pools::create(RuntimeOrigin::signed(10), 40, 10, 10, 10));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: 40 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
PoolsEvent::Created { depositor: 10, pool_id: 1 },
PoolsEvent::Bonded { member: 10, pool_id: 1, bonded: 40, joined: true },
]
);
// have two members join
let bond = 20;
assert_ok!(Pools::join(RuntimeOrigin::signed(20), bond, 1));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Bonded { stash: POOL1_BONDED, amount: bond }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Bonded { member: 20, pool_id: 1, bonded: bond, joined: true }]
);
// progress and unbond.
CurrentEra::<T>::set(Some(99));
assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, bond));
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Unbonded { stash: POOL1_BONDED, amount: bond }]
);
assert_eq!(
pool_events_since_last_call(),
vec![PoolsEvent::Unbonded {
member: 20,
pool_id: 1,
balance: bond,
points: bond,
era: 127
}]
);
// slash 50. This will be deducted only from the bonded pool and one of the unbonding pools.
CurrentEra::<T>::set(Some(100));
assert_eq!(BondedPools::<T>::get(1).unwrap().points, 40);
pallet_staking::slashing::do_slash::<Runtime>(
&POOL1_BONDED,
50,
&mut Default::default(),
&mut Default::default(),
100,
);
assert_eq!(
staking_events_since_last_call(),
vec![StakingEvent::Slashed { staker: POOL1_BONDED, amount: 50 }]
);
assert_eq!(
pool_events_since_last_call(),
vec![
// out of 20, 10 was taken.
PoolsEvent::UnbondingPoolSlashed { pool_id: 1, era: 127, balance: 10 },
// out of 40, all was taken.
PoolsEvent::PoolSlashed { pool_id: 1, balance: 0 }
]
);
});
}

View File

@ -1,273 +0,0 @@
// This file is part of Substrate.
// 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 frame_election_provider_support::VoteWeight;
use frame_support::{
assert_ok, derive_impl,
pallet_prelude::*,
parameter_types,
traits::{ConstU64, ConstU8},
PalletId,
};
use sp_runtime::{
traits::{Convert, IdentityLookup},
BuildStorage, FixedU128, Perbill,
};
type AccountId = u128;
type Nonce = u32;
type BlockNumber = u64;
type Balance = u128;
pub(crate) type T = Runtime;
pub(crate) const POOL1_BONDED: AccountId = 20318131474730217858575332831085u128;
pub(crate) const POOL1_REWARD: AccountId = 20397359637244482196168876781421u128;
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type Nonce = Nonce;
type RuntimeCall = RuntimeCall;
type Hash = sp_core::H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}
impl pallet_timestamp::Config for Runtime {
type Moment = u64;
type OnTimestampSet = ();
type MinimumPeriod = ConstU64<5>;
type WeightInfo = ();
}
parameter_types! {
pub static ExistentialDeposit: Balance = 5;
}
impl pallet_balances::Config for Runtime {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = RuntimeFreezeReason;
type MaxFreezes = ConstU32<1>;
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
}
pallet_staking_reward_curve::build! {
const I_NPOS: sp_runtime::curve::PiecewiseLinear<'static> = curve!(
min_inflation: 0_025_000,
max_inflation: 0_100_000,
ideal_stake: 0_500_000,
falloff: 0_050_000,
max_piece_count: 40,
test_precision: 0_005_000,
);
}
parameter_types! {
pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
pub static BondingDuration: u32 = 3;
}
impl pallet_staking::Config for Runtime {
type Currency = Balances;
type CurrencyBalance = Balance;
type UnixTime = pallet_timestamp::Pallet<Self>;
type CurrencyToVote = ();
type RewardRemainder = ();
type RuntimeEvent = RuntimeEvent;
type Slash = ();
type Reward = ();
type SessionsPerEra = ();
type SlashDeferDuration = ();
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type BondingDuration = BondingDuration;
type SessionInterface = ();
type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
type NextNewSession = ();
type MaxExposurePageSize = ConstU32<64>;
type ElectionProvider =
frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
type GenesisElectionProvider = Self::ElectionProvider;
type VoterList = VoterList;
type TargetList = pallet_staking::UseValidatorsMap<Self>;
type NominationsQuota = pallet_staking::FixedNominationsQuota<16>;
type MaxUnlockingChunks = ConstU32<32>;
type MaxControllersInDeprecationBatch = ConstU32<100>;
type HistoryDepth = ConstU32<84>;
type EventListeners = Pools;
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
type WeightInfo = ();
type DisablingStrategy = pallet_staking::UpToLimitDisablingStrategy;
}
parameter_types! {
pub static BagThresholds: &'static [VoteWeight] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000];
}
type VoterBagsListInstance = pallet_bags_list::Instance1;
impl pallet_bags_list::Config<VoterBagsListInstance> for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type BagThresholds = BagThresholds;
type ScoreProvider = Staking;
type Score = VoteWeight;
}
pub struct BalanceToU256;
impl Convert<Balance, sp_core::U256> for BalanceToU256 {
fn convert(n: Balance) -> sp_core::U256 {
n.into()
}
}
pub struct U256ToBalance;
impl Convert<sp_core::U256, Balance> for U256ToBalance {
fn convert(n: sp_core::U256) -> Balance {
n.try_into().unwrap()
}
}
parameter_types! {
pub const PostUnbondingPoolsWindow: u32 = 10;
pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
}
impl pallet_nomination_pools::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type Currency = Balances;
type RuntimeFreezeReason = RuntimeFreezeReason;
type RewardCounter = FixedU128;
type BalanceToU256 = BalanceToU256;
type U256ToBalance = U256ToBalance;
type Staking = Staking;
type PostUnbondingPoolsWindow = PostUnbondingPoolsWindow;
type MaxMetadataLen = ConstU32<256>;
type MaxUnbonding = ConstU32<8>;
type MaxPointsToBalance = ConstU8<10>;
type PalletId = PoolsPalletId;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub enum Runtime {
System: frame_system,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
Staking: pallet_staking,
VoterList: pallet_bags_list::<Instance1>,
Pools: pallet_nomination_pools,
}
);
pub fn new_test_ext() -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut storage = frame_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
let _ = pallet_nomination_pools::GenesisConfig::<Runtime> {
min_join_bond: 2,
min_create_bond: 2,
max_pools: Some(3),
max_members_per_pool: Some(5),
max_members: Some(3 * 5),
global_max_commission: Some(Perbill::from_percent(90)),
}
.assimilate_storage(&mut storage)
.unwrap();
let _ = pallet_balances::GenesisConfig::<Runtime> {
balances: vec![(10, 100), (20, 100), (21, 100), (22, 100)],
}
.assimilate_storage(&mut storage)
.unwrap();
let mut ext = sp_io::TestExternalities::from(storage);
ext.execute_with(|| {
// for events to be deposited.
frame_system::Pallet::<Runtime>::set_block_number(1);
// set some limit for nominations.
assert_ok!(Staking::set_staking_configs(
RuntimeOrigin::root(),
pallet_staking::ConfigOp::Set(10), // minimum nominator bond
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
pallet_staking::ConfigOp::Noop,
));
});
ext
}
parameter_types! {
static ObservedEventsPools: usize = 0;
static ObservedEventsStaking: usize = 0;
static ObservedEventsBalances: usize = 0;
}
pub(crate) fn pool_events_since_last_call() -> Vec<pallet_nomination_pools::Event<Runtime>> {
let events = System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Pools(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = ObservedEventsPools::get();
ObservedEventsPools::set(events.len());
events.into_iter().skip(already_seen).collect()
}
pub(crate) fn staking_events_since_last_call() -> Vec<pallet_staking::Event<Runtime>> {
let events = System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| if let RuntimeEvent::Staking(inner) = e { Some(inner) } else { None })
.collect::<Vec<_>>();
let already_seen = ObservedEventsStaking::get();
ObservedEventsStaking::set(events.len());
events.into_iter().skip(already_seen).collect()
}

View File

@ -1,7 +1,7 @@
[package]
name = "ghost-slow-clap"
version = "0.4.29"
description = "DEPRECATED: Applause protocol for the EVM bridge."
version = "0.4.30"
description = "Applause protocol for the EVM bridge"
license.workspace = true
authors.workspace = true
edition.workspace = true

View File

@ -20,7 +20,6 @@ pub fn prepare_evm_network<T: Config>(network_id: NetworkIdOf<T>) {
rate_limit_delay: 69,
block_distance: 69,
network_type: ghost_networks::NetworkType::Evm,
network_curve: ghost_networks::NetworkCurve::Secp256k1,
gatekeeper: b"0x4d224452801ACEd8B2F0aebE155379bb5D594381".to_vec(),
topic_name: b"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef".to_vec(),
incoming_fee: 0,

View File

@ -41,6 +41,7 @@ use ghost_networks::{
NetworkData, NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler,
NetworkType,
};
use ghost_traits::evictor::StakingEvictor;
use ghost_traits::exposure::ExposureListener;
pub mod migrations;
@ -50,7 +51,7 @@ mod benchmarking;
mod mock;
mod tests;
pub mod bitmap;
mod bitmap;
mod deserialisations;
mod evm_types;
@ -309,6 +310,7 @@ pub mod pallet {
>;
type DisabledValidators: DisabledValidators;
type ExposureListener: ExposureListener<BalanceOf<Self>, Self::AccountId>;
type StakingEvictor: StakingEvictor<Self::AccountId>;
#[pallet::constant]
type MaxAuthorities: Get<u32>;
@ -1455,7 +1457,15 @@ impl<T: Config> Pallet<T> {
let authority_index = index as AuthIndex;
if offence_bitmap.exists(&authority_index) {
weight.saturating_accrue(T::DbWeight::get().reads(2));
if let Some(account_id) = T::ExposureListener::get_account_by_index(index) {
weight.saturating_accrue(T::DbWeight::get().reads(1));
if T::StakingEvictor::try_evict_validator(&account_id) {
weight.saturating_accrue(T::DbWeight::get().writes(2));
}
}
if let Some(full_id) = <T::ValidatorSet as ValidatorSetWithIdentification<
T::AccountId,
>>::IdentificationOf::convert(id.clone())
@ -1637,7 +1647,7 @@ impl<Offender: Clone> Offence<Offender> for SlowClapOffence<Offender> {
missed_percent.saturating_mul(Perbill::from_rational(1, offenders_count))
}
OffenceType::CommitmentOffence => offenders_count
.checked_sub(Perbill::from_percent(9).mul_ceil(self.validator_set_count))
.checked_sub(Perbill::from_percent(9).mul_floor(self.validator_set_count))
.map(|threshold| {
Perbill::from_rational(threshold.saturating_mul(4), self.validator_set_count)
.square()

View File

@ -5,6 +5,7 @@ use frame_support::{
traits::{ConstU32, ConstU64},
};
use frame_system::EnsureRoot;
use ghost_traits::evictor::StakingEvictor;
use pallet_session::historical as pallet_session_historical;
use sp_runtime::{
curve::PiecewiseLinear,
@ -36,6 +37,7 @@ frame_support::construct_runtime!(
);
parameter_types! {
pub static EvicatedValidators: Vec<u64> = vec![];
pub static FixedValidators: Vec<u64> = vec![0, 1, 2, 3];
}
@ -172,6 +174,25 @@ impl pallet_balances::Config for Runtime {
type Balance = u64;
pub struct TestStakingEvictor;
impl StakingEvictor<u64> for TestStakingEvictor {
fn try_evict_validator(who: &u64) -> bool {
if !FixedValidators::get().contains(who) {
return false;
}
let mut evicted_validators = EvicatedValidators::get();
match evicted_validators.iter().position(|x| x == who) {
Some(_) => false,
None => {
evicted_validators.push(*who);
EvicatedValidators::set(evicted_validators);
true
}
}
}
}
pub struct TestExposureListener;
impl ExposureListener<Balance, u64> for TestExposureListener
where
@ -217,6 +238,7 @@ impl Config for Runtime {
type ReportUnresponsiveness = OffenceHandler;
type DisabledValidators = Session;
type ExposureListener = TestExposureListener;
type StakingEvictor = TestStakingEvictor;
type MaxAuthorities = ConstU32<5>;
type ApplauseThreshold = ConstU32<500_000_000>;

View File

@ -1691,6 +1691,40 @@ fn migration_from_v2_to_v3_works_fine() {
});
}
#[test]
fn validators_are_evicted_during_offence() {
let (network_id, _, _) = generate_unique_hash(None, None, None, None, None);
new_test_ext().execute_with(|| {
let session_index = advance_session_and_get_index();
prepare_evm_network(None, None);
assert_eq!(BlockCommitments::<Runtime>::get(network_id).len(), 0);
System::set_block_number(5 * EpochDuration::get() / 2);
let last_stored_block = 69;
for i in 0..3 {
assert_ok!(do_block_commitment(
session_index,
network_id,
i,
last_stored_block,
));
}
assert_eq!(EvicatedValidators::get().len(), 0);
let current_block = SlowClap::current_block_number();
SlowClap::on_initialize(current_block);
System::assert_has_event(RuntimeEvent::SlowClap(
crate::Event::SomeAuthoritiesDelayed {
delayed: vec![(3, 3)],
},
));
assert_eq!(EvicatedValidators::get().len(), 1);
});
}
fn assert_clapped_amount(
session_index: &SessionIndex,
unique_hash: &H256,
@ -1725,7 +1759,6 @@ fn prepare_evm_network(
rate_limit_delay: 69,
block_distance: 69,
network_type: ghost_networks::NetworkType::Evm,
network_curve: ghost_networks::NetworkCurve::Secp256k1,
gatekeeper: get_gatekeeper(),
topic_name: get_topic_name(),
incoming_fee: maybe_incoming_commission.unwrap_or_default(),

View File

@ -1,7 +1,6 @@
[package]
name = "ghost-traits"
version = "0.4.2"
description = "Shared traits including `GhostHasher`, `NetworkDataBasicHandler`, `BoundedBTreeMap`, `MerkleTree` and more."
version = "0.3.33"
license.workspace = true
authors.workspace = true
edition.workspace = true
@ -9,24 +8,14 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
num-traits = { workspace = true }
frost-core = { workspace = true }
rand_chacha = { workspace = true }
frame-support = { workspace = true }
sp-arithmetic = { workspace = true }
sp-runtime = { workspace = true }
sp-core = { workspace = true }
sp-std = { workspace = true }
[features]
default = ["std"]
std = [
"frame-support/std",
"num-traits/std",
"sp-arithmetic/std",
"rand_chacha/std",
"frost-core/std",
"sp-runtime/std",
"sp-core/std",
"sp-std/std",
]

View File

@ -1,62 +0,0 @@
use sp_arithmetic::traits::AtLeast8BitUnsigned;
use sp_runtime::traits::UniqueSaturatedInto;
use sp_std::vec::Vec;
pub trait BoundedBitmapMetadata {
fn total_bits_capacity() -> u32;
fn chunk_size() -> u32;
}
pub trait BoundedBitmapWriter {
fn insert<N: Copy + TryInto<usize>>(&mut self, i: N) -> Option<N>;
fn remove<N: Copy + TryInto<usize>>(&mut self, i: N) -> Option<N>;
fn truncate(&mut self);
fn nullify(&mut self);
}
pub trait BoundedBitmapReader {
fn active_bits_count(&self) -> u32;
fn highest_bit<N>(&self) -> Option<N>
where
usize: UniqueSaturatedInto<N>;
fn count_ones<N>(&self) -> N
where
u32: UniqueSaturatedInto<N>;
fn count_zeros<N>(&self) -> N
where
u32: UniqueSaturatedInto<N>;
fn is_empty(&self) -> bool;
fn contains<N>(&self, index: N) -> bool
where
N: UniqueSaturatedInto<usize>;
}
pub trait BoundedBitmapGenerator {
fn empty<N>(n: N) -> Self
where
N: UniqueSaturatedInto<usize>;
fn all_ones<N>(n: N) -> Self
where
N: UniqueSaturatedInto<usize>;
fn empty_from(bitmap: &Self) -> Self;
fn from_bitmask<Outer>(values: &Vec<Outer>) -> Self
where
Outer: AtLeast8BitUnsigned + num_traits::PrimInt;
fn from_vec<Outer>(values: &Vec<Outer>) -> Self
where
Outer: AtLeast8BitUnsigned + TryInto<usize> + PartialEq + Copy;
}
pub trait BoundedBitmapIterable {
type IntoIter<'a, ItemType>: Iterator<Item = ItemType> + 'a
where
Self: 'a,
ItemType: 'static,
u32: UniqueSaturatedInto<ItemType>;
fn iter<ItemType>(&self) -> Self::IntoIter<'_, ItemType>
where
ItemType: 'static,
u32: UniqueSaturatedInto<ItemType>;
}

View File

@ -0,0 +1,3 @@
pub trait StakingEvictor<AccountId> {
fn try_evict_validator(who: &AccountId) -> bool;
}

View File

@ -1,120 +0,0 @@
use frost_core::{Ciphersuite, Group, Identifier};
use rand_chacha::rand_core::{CryptoRng, RngCore};
use sp_std::{
collections::btree_map::BTreeMap,
marker::PhantomData,
vec::Vec,
};
pub trait IdentifierConverter<A, E> {
fn non_zero_index(index: A) -> Result<A, E>;
fn convert_identifier_to_index(bytes: &[u8]) -> Result<A, E>;
}
pub trait FlexibleRoundOptimizedSchnorrThresholdSignature<A, E>: MerkleTreeBuilder<A, E> {
type Packages<'a>;
type NoncePackages<'a>;
fn header_bytes_len(&self) -> usize;
fn element_bytes_len(&self) -> usize;
fn scalar_bytes_len(&self) -> usize;
fn verify_signature_share(
&self,
i: A,
is: impl Iterator<Item = A>,
ss: &[u8],
bf: &[u8],
nh: &[u8],
bh: &[u8],
gc: &[u8],
vs: &[u8],
vk: &[u8],
m: &[u8],
) -> Result<(), E>;
fn is_signature_valid(&self, vk: &[u8], r: &[u8], z: &[u8], m: &[u8]) -> Result<(), E>;
fn accumulate_signature_scalar(&self, ss: &[u8], sh: &[u8]) -> Result<Vec<u8>, E>;
fn generate_nonce_commitment<R>(&self, sk: &[u8], r: R) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), E>
where
R: CryptoRng + RngCore;
fn generate_group_commitment(
&self,
a: A,
sc: &Self::NoncePackages<'_>,
vk: &[u8],
m: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, Vec<Self::Hash>, Self::Hash), E>;
fn init_signing_package(&self, n: &Self::NoncePackages<'_>, m: &[u8]) -> Result<Vec<u8>, E>;
fn partial_sign_message(&self, sp: &[u8], sn: &[u8], ss: &[u8]) -> Result<Vec<u8>, E>;
}
pub trait EllipticCurveDiffieHellman<A, C, D, K, E> {
fn ecdh_encrypt_package<R>(&self, c: &C, aad: &[u8], m: &[u8], r: R) -> Result<D, E>
where
R: RngCore + CryptoRng;
fn ecdh_decrypt_package(&self, c: &C, d: &D, aad: &[u8]) -> Result<Vec<u8>, E>;
fn ecdh_prepare_additional_info(&self, si: A, ri: A, di: K, _m: PhantomData<D>) -> Vec<u8>;
fn ecdh_get_cipher_from_keys(&self, rp: &[u8], sp: &[u8], i: &[u8], _m: PhantomData<D>) -> Result<C, E>;
}
pub trait DistributedKeyGeneration<A, E>: MerkleTreeBuilder<A, E> {
type RoundPackages;
type Part1Result;
type Part2Result;
type Part3Result;
fn dkg_narrow_round_max_signers(&self, s: &[u8], m: A, r: u8) -> Result<Vec<u8>, E>;
fn dkg_verify_proof_of_knowledge(&self, i: A, r1: &[u8]) -> Result<A, E>;
fn dkg_part1<R>(&self, i: A, max: A, min: A, rng: R) -> Self::Part1Result
where
R: RngCore + CryptoRng;
fn dkg_part2(&self, r1_s: &[u8], r1_pm: &Self::RoundPackages) -> Self::Part2Result;
fn dkg_verify_private_package(&self, i: A, r1: &[u8], r2: &[u8]) -> Result<(), E>;
fn dkg_part3(
&self,
i: A,
r2_s: &[u8],
r1_pm: &Self::RoundPackages,
r2_pm: &Self::RoundPackages,
) -> Self::Part3Result;
fn dkg_derive_verifying_share(&self, sk: &[u8]) -> Result<Vec<u8>, E>;
fn dkg_decompress_key(&self, vk: &[u8]) -> Result<Vec<u8>, E>;
}
pub trait MerkleTreeBuilder<A, E>: IdentifierConverter<A, E> {
type Hash;
fn build_merkle_tree<C, V, F>(
verifying_shares: &BTreeMap<Identifier<C>, V>,
serialize_fn: F,
) -> Result<Vec<Self::Hash>, E>
where
C: frost_core::Ciphersuite,
<<C as Ciphersuite>::Group as Group>::Field: frost_core::Field,
F: Fn(&V) -> Result<Vec<u8>, E>;
fn generate_merkle_proof_from_tree<C, V, F>(
verifying_shares: &BTreeMap<Identifier<C>, V>,
merkle_tree: &[Self::Hash],
authority_index: A,
serialize_fn: F,
) -> Result<(Vec<u8>, Vec<Self::Hash>), E>
where
C: frost_core::Ciphersuite,
<<C as Ciphersuite>::Group as Group>::Field: frost_core::Field,
F: Fn(&V) -> Result<Vec<u8>, E>;
fn verify_merkle_proof(
&self,
verifying_share: &[u8],
merkle_proof: &[Self::Hash],
merkle_root: Self::Hash,
authority_index: A,
) -> Result<(), E>;
}

View File

@ -0,0 +1,10 @@
use sp_runtime::traits::AtLeast32BitUnsigned;
pub trait ExposureListener<Balance: AtLeast32BitUnsigned, AccountId> {
fn get_accounts_by_indexes(
indexes: impl Iterator<Item = usize>,
) -> sp_std::prelude::Vec<AccountId>;
fn get_account_by_index(index: usize) -> Option<AccountId>;
fn get_total_exposure() -> Balance;
fn get_validator_exposure(index: &AccountId) -> Balance;
}

View File

@ -1,27 +0,0 @@
use sp_std::vec::Vec;
pub trait GhostHasher {
type Hash: PartialEq + Copy + AsRef<[u8]> + AsMut<[u8]>;
type HashBytes: AsRef<[u8]> + AsMut<[u8]> + TryFrom<Vec<u8>> + Default;
fn from_slice(bytes: &[u8]) -> Self::Hash {
let mut target_hash = Self::empty();
let hash_buffer = target_hash.as_mut();
let copy_len = bytes.len().min(hash_buffer.len());
hash_buffer[..copy_len].copy_from_slice(&bytes[..copy_len]);
target_hash
}
fn hash_to_bytes(hash: Self::Hash) -> Self::HashBytes {
let mut bytes = Self::HashBytes::default();
bytes.as_mut().copy_from_slice(hash.as_ref());
bytes
}
fn hash(data: &[u8]) -> Self::Hash;
fn empty() -> Self::Hash;
fn hash_len() -> usize;
}

View File

@ -1,6 +1,5 @@
#![cfg_attr(not(feature = "std"), no_std)]
pub mod bounded_bitmap;
pub mod exodus;
pub mod hashing;
pub mod evictor;
pub mod exposure;
pub mod networks;

View File

@ -1,12 +0,0 @@
use crate::converter::IdentifierConverter;
use sp_std::vec::Vec;
use sp_core::H256;
pub trait MerkleTree<A, E>: IdentifierConverter<A, E> {
type VerifyingShares;
type Hash;
fn merkle_build_tree(vs: &Self::VerifyingShares) -> Result<Vec<Self::Hash>, E>;
fn merkle_generate_proof_from_tree(vsm: &Self::VerifyingShares, t: &[Self::Hash], i: A) -> Result<(Vec<u8>, Vec<H256>), E>;
fn merkle_verify_proof(&self, vs: &[u8], p: &[Self::Hash], r: Self::Hash, i: A) -> Result<(), E>;
}

View File

@ -1,26 +1,10 @@
use frame_support::{pallet_prelude::*, storage::PrefixIterator};
use sp_std::vec::Vec;
use sp_runtime::{
offchain::http::PendingRequest,
traits::{AtLeast32BitUnsigned, Member},
DispatchResult,
};
pub trait NetworkDataBasicHandler {
type NetworkCurve: Parameter
+ Member
+ Default
+ Copy
+ TypeInfo
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type NetworkType: Parameter
+ Member
+ Default
+ Copy
+ TypeInfo
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type NetworkId: Parameter
+ Member
+ AtLeast32BitUnsigned
@ -34,13 +18,10 @@ pub trait NetworkDataBasicHandler {
pub trait NetworkDataInspectHandler<Network>: NetworkDataBasicHandler {
fn count() -> u32;
fn contains_key(n: &Self::NetworkId) -> bool;
fn curve_exists(c: &Self::NetworkCurve) -> bool;
fn network_for_block(b: impl Into<usize>) -> Option<(Self::NetworkId, Network)>;
fn get(n: &Self::NetworkId) -> Option<Network>;
fn iter() -> PrefixIterator<(Self::NetworkId, Network)>;
fn iter_indexes() -> impl Iterator<Item = Self::NetworkId>;
fn iter_curves() -> impl Iterator<Item = Self::NetworkCurve>;
fn iter_types_by_curve(n: &Self::NetworkCurve) -> impl Iterator<Item = Self::NetworkType>;
}
pub trait NetworkDataMutateHandler<Network, Balance>: NetworkDataInspectHandler<Network> {
@ -60,27 +41,3 @@ pub trait NetworkDataMutateHandler<Network, Balance>: NetworkDataInspectHandler<
fn accumulate_incoming_imbalance(amount: &Balance) -> Result<Balance, ()>;
fn accumulate_commission(commission: &Balance) -> Result<Balance, ()>;
}
pub trait NetworkRpcResolver<W, B, H, I, S> {
fn get_block_request_body(&self, id: I) -> Vec<u8>;
fn get_hash_request_body(
&self,
id: I,
block: B,
session: S,
contract_ref: &[u8],
selector_ref: &[u8],
) -> Vec<u8>;
fn parse_block_requests(
&self,
pending_requests: Vec<PendingRequest>,
deadline: sp_runtime::offchain::Timestamp,
request_id: I,
) -> Vec<B>;
fn parse_hash_requests(
&self,
pending_requests: Vec<PendingRequest>,
deadline: sp_runtime::offchain::Timestamp,
request_id: I,
) -> Vec<(H, S)>;
}

View File

@ -1,8 +0,0 @@
[package]
name = "ghost-types"
version = "0.0.1"
license.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true

View File

@ -1 +0,0 @@
pub

View File

@ -1,79 +0,0 @@
[package]
name = "ghost-weaver"
version = "0.0.0"
description = "Weaving a secure cryptographic tapestry across different external chains."
license.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
log = { workspace = true }
hex = { workspace = true }
num-traits = { workspace = true }
parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["alloc"] }
codec = { workspace = true, features = ["max-encoded-len"] }
scale-info = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-balances = { workspace = true, optional = true }
sp-keystore = { workspace = true, optional = true }
sp-application-crypto = { workspace = true }
sp-runtime = { workspace = true }
sp-arithmetic = { workspace = true }
sp-core = { workspace = true }
sp-std = { workspace = true }
sp-io = { workspace = true }
ghost-networks = { workspace = true }
ghost-helpers = { workspace = true }
ghost-traits = { workspace = true }
[dev-dependencies]
sp-keystore = { workspace = true }
[features]
default = ["std"]
std = [
"log/std",
"hex/std",
"codec/std",
"num-traits/std",
"scale-info/std",
"frame-support/std",
"frame-system/std",
"frame-benchmarking?/std",
"sp-application-crypto/std",
"sp-runtime/std",
"sp-arithmetic/std",
"sp-keystore?/std",
"sp-core/std",
"sp-std/std",
"sp-io/std",
"ghost-networks/std",
"ghost-helpers/std",
"ghost-traits/std",
"pallet-balances/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"ghost-networks/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"ghost-helpers/runtime-benchmarks",
"sp-keystore"
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"ghost-networks/try-runtime",
"pallet-balances/try-runtime",
]

View File

@ -1,415 +0,0 @@
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;
use sp_std::vec;
use ghost_helpers::networks::NetworkDataBuilder;
fn prepare_pallet<T: Config>(
authorities_len: u32
) -> (NetworkIdOf<T>, T::AuthorityId, AuthIndex) {
let authority = T::AuthorityId::generate_pair(None);
let authorities = vec![authority.clone(); authorities_len as usize];
let bounded_authorities = WeakBoundedVec::<_, T::MaxAuthorities>::try_from(authorities.clone())
.expect("more than the maximum number of keys provided");
Authorities::<T>::put(bounded_authorities);
let network_id = NetworkIdOf::<T>::default();
let network_data = NetworkDataBuilder::default()
.with_network_type(NetworkType::Utxo)
.with_gatekeeper(
// Transaction: https://blockstream.info/tx/c41999f86269d5a22f39e18530b065854f447ef64a48be4013c4a9faa657f418
hex::decode("c6e4e0f822c31540b4b05903ce3c67b6cff8e6d4000000000000000000000000")
.expect("Bytes32 from UTXO should be valid"),
)
.build();
T::NetworkDataHandler::register(network_id, network_data)
.expect("network should be valid for insertion");
(network_id, authority, 0)
}
#[benchmarks(where T: Config)]
mod benchmarks {
use super::*;
#[benchmark]
fn extend_warp(
a: Linear<1, { T::MaxAuthorities::get() as u32 }>,
) -> Result<(), BenchmarkError> {
let (network_id, authority, authority_index) = prepare_pallet::<T>(a);
TapestryDrafts::<T>::remove(&network_id);
let block_attestation = AttestationContext::default()
.with_network_id(network_id)
.with_authority_index(authority_index)
.build_block_attestation(420);
let signature = authority
.sign(&block_attestation.encode())
.expect("Signature should be created");
#[extrinsic_call]
_(RawOrigin::None, block_attestation, signature);
let current_attestations = NetworkAttestations::<T>::get(&network_id);
assert_eq!(current_attestations.len(), a as usize);
assert_eq!(current_attestations[authority_index as usize], Some(420));
Ok(())
}
#[benchmark]
fn weave_weft(
a: Linear<1, { T::MaxAuthorities::get() as u32 }>
) -> Result<(), BenchmarkError> {
let weaving_session = 420;
let next_session = weaving_session + 1;
let root_hash = H256::repeat_byte(0x69);
let (network_id, authority, authority_index) = prepare_pallet::<T>(a);
let tapestry_key = (network_id, weaving_session, next_session, root_hash);
TapestryStrands::<T>::mutate(&tapestry_key, |tapestry| {
(0..a).for_each(|i| {
let index = i as AuthIndex;
if index == authority_index { return; }
tapestry.insert(index);
});
});
let current_block = T::BlockNumberProvider::current_block_number();
let dummy_draft = TapestryDraftBuilder::default()
.with_current_block(current_block)
.with_block_delay(T::WeavingDelay::get())
.with_median_external(1337 as ExternalBlockNumber)
.build();
TapestryDrafts::<T>::insert(&network_id, dummy_draft);
let hash_attestation = AttestationContext::default()
.with_network_id(network_id)
.with_weaving_session(weaving_session)
.with_authority_index(authority_index)
.build_hash_attestation(root_hash, next_session);
let signature = authority
.sign(&hash_attestation.encode())
.expect("Signature should be created");
let tapestry_strand_key = (network_id, weaving_session, next_session, root_hash);
let weaving_state = WeavingStates::<T>::get(&network_id);
assert_eq!(weaving_state.hash, H256::default());
assert_eq!(weaving_state.next_session, 0);
assert_eq!(weaving_state.count, 0);
assert!(!TapestryStrands::<T>::get(&tapestry_strand_key).contains(authority_index));
assert_eq!(
TapestryStrands::<T>::get(&tapestry_strand_key).count_ones::<u32>(),
a - 1
);
#[extrinsic_call]
_(RawOrigin::None, hash_attestation, signature);
let new_weaving_state = WeavingStates::<T>::get(&network_id);
assert_eq!(new_weaving_state.hash, root_hash);
assert_eq!(new_weaving_state.next_session, next_session);
assert_eq!(new_weaving_state.count, a as AuthIndex);
assert!(TapestryStrands::<T>::get(&tapestry_strand_key).contains(authority_index));
assert_eq!(
TapestryStrands::<T>::get(&tapestry_strand_key).count_ones::<u32>(),
a
);
Ok(())
}
#[benchmark]
fn pull_utxo_thread(
a: Linear<1, { T::MaxAuthorities::get() as u32 }>,
) -> Result<(), BenchmarkError> {
let session = 69;
let (network_id, _, _) = prepare_pallet::<T>(1);
// https://blockstream.info/tx/6fdcb55eaeddae87fdbbd3e6fd8c2b80c1e7e7c1214e1a6e35c06a70e5f21fba?expand
// NOTE: this is the best transaction that matches the max sizes I found,
// would be nice to find exact edge case
let root_string =
hex::decode("00000000000000000000662f626dbc3c06161a324d879fadcd0293d8e0884a1d")
.expect("Bitcoin Root hex should be valid");
let expected_root = H256::from_slice(&root_string);
LoomStates::<T>::insert(&network_id, &session, expected_root);
let receiver_32: [u8; 32] =
hex::decode("6969696969696969696969696969696969696969696969696969696969696969")
.expect("Bytes32 from UTXO should be valid")
.try_into()
.expect("Receiver address must be exactly 32 bytes");
let receiver_account = sp_runtime::AccountId32::from(receiver_32);
let transfer_amount: BalanceOf<T> = 140_693u64.unique_saturated_into();
let payload = ThreadPayload::new(12, transfer_amount, receiver_account);
let utxo_thread_proof = UtxoThreadProof::new(
1,
0,
transfer_amount,
payload,
hex::decode("008008201643a05d24fe1f015bf472fdedd8ef39dd8247568d3901000000000000000000569ac8f0aed0c5b632dd1dcc031f3d826a170e13cd8176430a2873a33d4694c2501f6a6ad43a02178dbac0d66b1600000e88be94dbcedc3f32bce789eb4512c68b39b91b54c4f2913e84baf7bae21bdc466d954b10876c1e8ad6591461027236c9a471883f96c028a7d1f08deb833f56fa18f457a6faa9c41340be484af67e444f8565b03085e1392fa2d56962f89919c4fcfff99fc6e92be067d01028fb54e8dc1e8425f0ef6bf1cc64220a2ba22163666eb01f99648e8c2af1bb1a0e1ba297f0244a09aa6a6ff87092312b02e8d75aef61e5c08f5b59184a45855e987c5ea114e5da61e55337bfae99f3a2e9601e72ae6036633dc97631609813a30d1185f98e7d6effc4a0330db0394da388c0058e9a730a62d8c679c35bf8d270ca8b95cec7d848729cdfd94a284f060f353b379bfc36310ac78306183a056a04156efd7e77661ce7ba50a4ef9f9e6b3681dfb39d8d0161ad0d2ec46834b5f270b67aa63a8a5b69287e8606b8a438785f8a5af66f84a2641438ba4a5fa22029227e9c2ddb6f23867b112c027d2fc2cf031a46c874701768b5c4983959b98d98736a2ed5ab0dd064ee24e49a3a7dac63e8b857ad4c8b5aa0cf22a7eac3180f3a54630b486de928ecf590e807d2e821818e2fe7e86610187d6673a6f95c639af80a7a7710b0088d44057ca61bdf5f4d5ba68d46b88e4304ffb70000").expect("Bytes32 from UTXO should be valid"),
hex::decode("020000000001029ee4235efd4a3cb183d4c311ce858eeee7748263a28afd3d861bac1cb4a8d368010000000001000000d7c5869bfffaa1877d0ecd37e46dc70c232a16f7955e635d57f926d1b3e640bf0100000000ffffffff02ff490b0000000000160014f635b17877709ba3c5acbe153f38a47a733f5b169525020000000000160014c6e4e0f822c31540b4b05903ce3c67b6cff8e6d40247304402200f2f3763fad59d38b4e101b9675450c0a54d45fe17e2b1babde0d0ce6051ea5b0220643e71d66752c0f1bd30dc0897305da77f223d14c781e86063ac181ad7fb90b6012102aad4cca3dab6427bc754a5a123de32cd037e7177fade9f72d83088b00066cffc0247304402200c8f1ff8230813e5e4ab8ec802c614bc8705c407c5958d64dd79cc8422d85a6202203ea832d581c404200b5974fd1a11abfe1ebac213842083cb02eba3f253c8c2aa012102d70df1d4d58701adab82b4816e0640c27403d1fab859ca9f47cb6eccca3dd96200000000").expect("Bytes32 from UTXO should be valid"),
vec![
hex::decode("00a01520c1d508977a3787523a0f959e14a8df763dc87927c7910000000000000000000036303f2bcf94f752a1d4b0641d1adbc2d80aa89628feefd7f837a74792e26ab8bb276a6ad43a0217bece2e64").expect("Bytes32 from UTXO should be valid"),
hex::decode("0400e020702ff95ecc1b8575cff205bfc24592c87ba09280bd84000000000000000000008ca9c40d5ab762f160661635cade50fd14571dcf5fdf8c463b617caf9b07dc5a3b296a6ad43a02171340d661").expect("Bytes32 from UTXO should be valid"),
hex::decode("000000345457ca4006ddc376db24b7a37db04dfa0b46c1d99267010000000000000000009409d64eb06a6dd83ae2937f70d07222d75505a07f12e756b61a27189851e3bbfe2c6a6ad43a0217a06575b5").expect("Bytes32 from UTXO should be valid"),
hex::decode("004007220e37af053c56a86899e1219107937fd5e83ff61aeb7800000000000000000000c0588f8b7a697ff9f367ec4a69a9998ef8f2552f808b5025b4b8f604d0a145c6123e6a6ad43a0217a662289d").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0ff3feeae586d6f7215f48e857ed7fcf823b27117f2805fe000000000000000000000dcaa345e44c85be86da35ab0f25e2f089f04f66c1e3a3881a8455b1e42ba2500ad3e6a6ad43a0217b3810985").expect("Bytes32 from UTXO should be valid"),
hex::decode("00600520214f05f95e93d1d46c768a910fe0a15da3de556c2a7a01000000000000000000a129b7d281efd307700bff535137324aa0c5decc9829243bb7b29f1cafcbbd6c114b6a6ad43a0217cd209b45").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000df2d608f226bda092f6d8da18e92e3cbb41d5c99c7ddbf20010000000000000000009848dfe4f23c9211df920b37d571b2a4363afac3efb83d7cf4249b6491a6c8dfff4c6a6ad43a02174646c9ff").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000020c69aef55529fcbf6d18aea8a701177317483d63c32d1000000000000000000000c3551d009eb18b1cc5da8420c5e1268c1de482127623b11f74743d46f3f34ebf84e6a6ad43a021796234549").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800820d50e734c76e83a3d2658d1977034372b68362927c06b010000000000000000007b701acfc56d4ef6086eda92cde2c809d6960dca65ec9e617d53e0761f620bb72e506a6ad43a0217416f2f2b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00009f23d562db91d625b878e92f5d410c04775ed88006a18a0300000000000000000000a7b1bbad02565047b4428e2ef0cf3acbd0714c77934e44abc367451b85c3e56be3556a6ad43a0217c3b1aebd").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000003e8c4ec35b8fd1f07d6ff39de67c1ade69a25df69d84030100000000000000000033d4dab1c4958ae6d9cb853278362cbf74d25c1c3c7150889883c7fbdfa5004506566a6ad43a021751a2160f").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a00020dfde782b9446c17ae40a86dcb2e308d4c729a46c162500000000000000000000cadaf134229eba7775042b3d8689553ba93d0ea929f411d3a1a01693b923b1bf22586a6ad43a02172393ef65").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800c204ec04712a453ed72818b60cb9f74931cc5a59bbcac0d0100000000000000000059eb7a758ff3809a2a5010a3e849201314378120b02b6386490c8b11ad56be308d5c6a6ad43a02177bc26d48").expect("Bytes32 from UTXO should be valid"),
hex::decode("00801935e8c63aa1065f46a7d31bc5063be04108bdd3b18dd326020000000000000000009a3cfbb7b79d1d51f11ebd0a28ed2bf29e53d36ccefddff12322ca47d00665076c5d6a6ad43a0217dc5d92d8").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000003644663bcc00a79e38e833e4a16805ae72cc373cb7e51c01000000000000000000a33b3c0378b2a2d213aac062c7cd8b2c2b3a9c11aaeb4824a037863e7bd5fe7701636a6ad43a02173b36822e").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002a4a1da985bd4aa15be31a03fd3efbe77ad0ebabc392ff0100000000000000000077eb14d0e30b97aa2baff829fa7754de88a359e6a3711300f4e2e0c1ad1435829d6b6a6ad43a0217c8ef1999").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000024a4efb58211a7c804ef24ee11f5f9f9db2509be9bc5da010000000000000000001cc7ac2fe2e38bfd4582367690b0521e71c30f0083f68565fd948ad4ee124fb91b6c6a6ad43a0217851e16c0").expect("Bytes32 from UTXO should be valid"),
hex::decode("0060e729537b191715a6f594c55e85574253c84580909a2fa72200000000000000000000fa2959118ac66e3569081e4645f87f9560742290b52ee54f7886b6fa5f83aa3d016d6a6ad43a021708a2ad64").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3f6fe5befcf9e4c77cf03673076ff822740622f75cd74e010000000000000000008da0cd1fb34cfd4e58b5d3f1e20f6104e491320d5dc109fe16a455e3d385adb90d6f6a6ad43a02175c26ad30").expect("Bytes32 from UTXO should be valid"),
hex::decode("00204120cd2dba55f0867cff62ee5bab27b0ba85ac58c869f55c00000000000000000000a685eaec339dbb6e4fa52dacd2752291d5bb55dceb0b8b359e5ecbc0b2d1f884506f6a6ad43a0217de2f0c8c").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e01420a16b828d829748aa9602930d3ef1d4c983b86b39a20101000000000000000000d6e8aecebb3748113be25a6522f47e10e63c57fe95a09f35052fdabcc8a68ad819736a6ad43a021729b35edd").expect("Bytes32 from UTXO should be valid"),
hex::decode("008067269f4c1aaf3129211de19714c6a5bb9c5481ea34f044da00000000000000000000734b07e26a80d3345fa74d1450a1b68d95e0714c163dc7b2b3eb9a0aef8b50c934756a6ad43a02172eba7608").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0fc20ed544115339aa1a59249f02f62856feb602ea083f93300000000000000000000d30c9e1e276efd3be62b7cd7194aa7fb0b807f2fecb9a56074247887b2299882f5756a6ad43a021702a2b823").expect("Bytes32 from UTXO should be valid"),
hex::decode("000000202d2c60af64f5b2a415bc46bca915042d5136b46929c501000000000000000000e0e3b49cd8b926a7468831c1b115e14838640cb735833561d551a804cf1a752cc3766a6ad43a0217d81e878d").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3f3c41153e725b51dc793b18c0bfd9cfaf14cd091e000402000000000000000000ff9b55b7ed1dcff73f458e48dd2799ae141829586ce0a3902a783f00981c798bff776a6ad43a021700849f4b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00200f237376e2f7ed32d9e0818617150255cc5c87bfc43d182c000000000000000000003c08d2ba07dda561bcb53f56fff0780c5d1046686c363743b17e1591de293a8538776a6ad43a02176fda963f").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800020f3ac5bb512764387873548d1cc0d4fef3b70077b6e700100000000000000000035accf350c72501ce6aedd556177cd99090cb4952c7b23fa424ac1f7a282c0315a786a6ad43a02174cf87729").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3fcee6362f1a42288aa20c3f21e9fcde46dcc1a9402d19010000000000000000006e40bfc29eb8859f30d083cefc45482f77946dcb429bcf1e064d1bcbd378f23c0c7b6a6ad43a02173d75f866").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002ae9ef834e71842c871391dd44a09960dd63990dd557e30100000000000000000052b6b1f008e5b0a5f38be6b2082ec17889619f0627f4064106e820364e1cc8072f7a6a6ad43a021740becfc5").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a0ab238496ac097daf6fee7acdd40ef1a503089d76db2256c400000000000000000000a6e564e0de9af79eb797a4b9afb26b5ad56395f3cbb7a9b4bde5a760d140aae8ba7a6a6ad43a021735b5f7f8").expect("Bytes32 from UTXO should be valid"),
hex::decode("00200320e3d603f57832afb5e67c782aa5fd5d8e2d655230b50e020000000000000000005451ec2c708395b414a347863a8cb7ee4a7d8b0309086af4c329c3ba164be5930d7c6a6ad43a02171ffe7b21").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000720159f0a42d5afd3196d6bc4745786b08017e9f2fe65c4010000000000000000002204634460a10b17be10462c08d3a3247e8955da5ec01f525fd0d58b14a5e2ebab826a6ad43a021737c8b684").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000022030d1a71c70fe8d8e994e1dcddcda8c8c5798ea9bb20d000000000000000000005d5579d7b5cc4c5e43d45169c93abf90dc6f212cd462407a48fc144d8315efb35b836a6ad43a0217aeaa2b47").expect("Bytes32 from UTXO should be valid"),
hex::decode("000000222a3e9205528e4d5618a7287d3d2370a41a6cd2d346fb0000000000000000000016d19ca5aad54bceea5245a221fddbaedb3f56fd6b8f12d59af65a167a7ab56aee836a6ad43a02173c540bc8").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0ff3f9ddcd3ff85fdcd54b32df85a1eb8648b211b72a247870000000000000000000043b56016ddc1c29bd76c92a831b4de36a59cc0fb88d294b6faf24339afd8d64de6886a6ad43a0217126de83e").expect("Bytes32 from UTXO should be valid"),
hex::decode("0060c424aa0dedbd17459541b2df58d403e78824d0b306e13888000000000000000000000d05d08f6044eb00963044e75b4dc14417fd7c3acf1f6d2f9eaf9432627d6a93c68a6a6ad43a0217971b219c").expect("Bytes32 from UTXO should be valid"),
hex::decode("00006c2235ade56a30b832a1d86a389853d9d7ef251dcd31d5de0000000000000000000028cbac80a576b4b03407029062c64268eae619201f84de43d65b6cc29d49945d598b6a6ad43a02178216ff0c").expect("Bytes32 from UTXO should be valid"),
hex::decode("0060c924fee183b290d1e5ba0dd2a211888cb9a45ac3261128ca01000000000000000000c64693de6d95445e23df797f3591719428ea07754aad0c5c03707a1696a49316e48c6a6ad43a02172c70405e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c09c217b4715e06ee536c88f2b04f8da84c9be46bb89b337cb0000000000000000000035ed0514da32bff54f234681d567b7bb8bebd48c1981e88a480a0069cf19fae9318f6a6ad43a0217578cf0e8").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c00f2089ef5721cd6bfe541cf56a30fe029e8b6a99d426b59b010000000000000000001cc5db1b35955722b2b072be5497665f2bcc0116021fc52ad106b86f85eb7a15ea8f6a6ad43a021726299c25").expect("Bytes32 from UTXO should be valid"),
hex::decode("00400a20b73d674783455e1728c82701ae1c5bab268563697aa001000000000000000000c7ef430a886118fb6bdd0536e867b9b6fc15cb198c7df4f4669c2fb94244445ebc916a6ad43a021757775d4f").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0012028d9ad98ee6302bffc27d2fc51e905e7ff1455e9ad9a000000000000000000005334bd5deb896d99d5f85f762ca5b5057bd8a11fb1f718f794ed67525ce5275639926a6ad43a021742a4ad22").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0982497ed444856620be9d4a2b3f2dd8385aef13794945fc4000000000000000000007a27f7d0b1a20ec5e2f5b69edb844ceb52acbe24cc44b1e486a86105591678c846926a6ad43a02176d3f1083").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800420a90444f33700b0a903d8dd28c7e4b00f1784a4f12c0801000000000000000000068f9295a349845eb2431428cfc318b72eff6d8b828b61533e5b9889be6a455f69926a6ad43a02176536c20e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0ff3f9efdf00d718c728b94e39dde5c42b0450539b1e0d20a02000000000000000000fff04185f99913ff903e7fff46f04e0ae0f132677d10b8fa1537b98be859020c3f946a6ad43a021759b8976c").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002ecf497d98fdd70adce90e8ce350f3d9f14b735f7cd03c010000000000000000003e3c02c9fd4bf5cde2f4f0ef50570669aa586b3b12e1dff52507337198d3d879bc966a6ad43a0217d2822239").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000522c9273bf66ca7ea531cd3409f3b7fb9f228966e67149300000000000000000000ad5c81834cc230f4ea59cd148298080d22cd008a440a75e34d946ea646f94e7791976a6ad43a0217899d978e").expect("Bytes32 from UTXO should be valid"),
hex::decode("008003203404ad959ad7bc9787388e0035542ad5d540f5077130010000000000000000005082dd1fd7487596fe92f1c06261d3f1abd2da19c2116b175d62adc245ae9ccfcf976a6ad43a0217eafb7678").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0ff3f6e2ede80814d029f614eca38ab9c9a15e9400a023d0200000000000000000000429fa6b33b892eb6544a11e0f3728d109acbeb0f9ad59a48ecd3d64e28543a4987986a6ad43a02179c1c0930").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a00820b709f5dde282465f55c4d7e66cb2ba6fc916c0d51c4c01000000000000000000eb37059a5ba1cdd3f3810607c8d194363679bc9f99dcd95652283a12d7603b29ed986a6ad43a02174b733dd8").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3f4b173d1bc45aa63418b7bba7c0fa5a5acfe53d22eb1e01000000000000000000f745f6a23d5cc180ff74ad724e3a12bf2bdf6d5d8f1cbacac629b28634fdd3b71c9c6a6ad43a02174c304983").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002830ec33bdc09b86dd4d9e637a2c5c4811989d9bc33505010000000000000000008caab6a74d42b54943d5ab85c80a95f17bdabaaccf27f8e1d18923fe2ad0113c2f9d6a6ad43a0217a53735c3").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a04c25fb6183fac04e92b6a4b899c9ef156babf959dce7c7b600000000000000000000adae556e5b521d5c392f39d3c8695d5cc11dd3234a6c38c7c423ac430fe5d10f2b9f6a6ad43a0217fd964416").expect("Bytes32 from UTXO should be valid"),
hex::decode("00606b202e489dafa11d28ef8c748ed8da4a954c5affb25454bb01000000000000000000e3f9ee43488ffbb8b711222f0540d1fd61bbec8ffbb9ced960dc2352392eeee180a06a6ad43a02170208ea28").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a00b207940814db8828f2f99a835fec3507ac1e3c6b8c7aaf100000000000000000000a4986016840beba7d215510f02f5e88cd41fc4cfc9bee4069ea56a4a7a9cae8715a26a6ad43a02177d018b27").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e04325f4a84a1a7043948c2af0faa632f4735b8dd989f49c250100000000000000000072707db2fc682148e6aa27d4dbdf7c20009db65c848132f76bc1b3a31bcddcb57ca46a6ad43a021777dae935").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0ff3fad0b6289eaa9c8932a76daadd7a5c3bef539fa19708b00000000000000000000dd2c965c6cea3dc01c21466b8b575c24f0b789a79c6e3f6ba7dcbe45740e1fbb9ea86a6ad43a02173bd6dce6").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000022064c18468116c4e6aa84afd708a1dc597a4aeb5e0d26001000000000000000000d0132ba263c4eb125280313ea30d4853b2201973b825588780c5e080eaaa8418c0a96a6ad43a0217d3de833b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c00e25575e08652190884e0ce0e963d24dc654eeeb47c77f1900000000000000000000068daf8886ba4daec55638e8f7e06890a186b6e6aefb5e7912b360a5a644bf1704aa6a6ad43a0217693c1a54").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002ccb2e57d6fa6db31eb2fe4b099c98602747060080927a010000000000000000004ae5c82107d3b01d762bd7250842757420ab88bd9eafa531e45ddd5460ac1c4524aa6a6ad43a0217ae883618").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000420385cfb56a9c0aa212ad2d8cfc3be492cb768e7be66e801000000000000000000e47b35c93528a98e5ccc07f7a64120f10fe44a7d2add76f851f2ef1ff86fb35588aa6a6ad43a0217e74aa794").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000002ae44f541085b616ee2b6b66143d45b8d9d005a13ee58601000000000000000000eb7efe30fbcae6ef197af5303e922d5f13af046afe6722a44a407de33cfb05ecd1aa6a6ad43a0217cc1c1fc5").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c00220ddf782b16cfb698ec47c721caa0e043c6e3045bd7d3f010000000000000000008ebe86fc87779b0ab7b1eeda4f938fa6bff75fdf3f03326d4c3f0f212db1a5df24ac6a6ad43a021787917f4b").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000003a8e0ffb97a26cb108666a56d11cfa7e8083858aefd35c00000000000000000000f741327a673e2680ea856c80fe7c45f495f35c0809c79095d2aa49bdac42d8a91eae6a6ad43a0217ee19f61c").expect("Bytes32 from UTXO should be valid"),
hex::decode("0420a12a06c61bdd24754ca5788b5b98127fa4cad6bfc6b4943000000000000000000000c0bc9db8ba6c3cd5e4852713723ab9462ed8cdff9a8e7a59f106a7d1e5d9596223af6a6ad43a021752931fe7").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a0cd2bcda5fed994deb88252ba588292824dafbfed2be7bb8300000000000000000000b021ceecadb5f6f0fcdec2d4dc38cc9021bec331a411769cdbfa1f0d731756c55fb16a6ad43a02174c791b12").expect("Bytes32 from UTXO should be valid"),
hex::decode("04e05c24b1a2e59ea42a7424eea8fdb9b95d65865090c6745fec01000000000000000000d49392556462b7cc6a324f3297b95655e8558a842829fa4dbe42a8e8740029149ab16a6ad43a021794875146").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a037310cb4c38e4d2d46702721cf104e9adf7a2120c0ddf85e01000000000000000000309c7334a84c50b5274a7efcc0ee46380415322cb2215a0220d84b7db6f96e6ac3b16a6ad43a0217d06362de").expect("Bytes32 from UTXO should be valid"),
hex::decode("00001c2a5253d07fb8f9c845066cd8d88ae9cbe590f2239d323a010000000000000000002430bbca343667d2cee8a3d7a86901914c852f38a92dcc8142f694235c3d8bf334b26a6ad43a0217dd6db83c").expect("Bytes32 from UTXO should be valid"),
hex::decode("00805c22ab4c07fb2ce6a683075bde23e7d94f50b588e8f9aa9c00000000000000000000ae3e39b85b23c403d31143839b34cc074e7eef0ecb1bf27c54e324a5be96676d92b26a6ad43a02177008583b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c004200a2cd7ef7703ae1fb2093c550eb744c3ea18108ae7a800000000000000000000d0bada625a70a2fd6cfaa2aede6d72bf4bdd10b64a52289820832323519d62ef43b56a6ad43a021767cab9b1").expect("Bytes32 from UTXO should be valid"),
hex::decode("008000206d1cce7f0978d68ffdf92d43c7027bdb81b612641586010000000000000000008d818799a9cada0f4f9243f2cad85b8940125850a9ba37f7d1e6fd282c8c62e145b76a6ad43a02179e855276").expect("Bytes32 from UTXO should be valid"),
hex::decode("0060af24e8f12bb06b4a71cf56a91e73a3966e14c17208a54d6501000000000000000000fbc1e7250797d3e71b49e1abaef047d8cf7e16f1c0ff9de688182d503d0cae67d6b86a6ad43a02171ebed5cc").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c00220511c75e44d1d627b986749aead0f55ab37f04df146ad0100000000000000000037e94c77e2c639bbec32dc4dd570b31516ea25fd1497156602b5e149f8d391d682c06a6ad43a02176534c3d3").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3f78fb96b3c3c7b29aa1915342bddc921bffd01bfbb63d010000000000000000007a5c1741944ca4332ef03742b971469c0c966655b31d10609a8ffbea38a94cfa2dc56a6ad43a0217a35c947e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e004204870e85ab18fcb38d67c35979efc5df4105490b6713f00000000000000000000c7714efab1a223e7fa21c6f87db62b32cdcd048d5b70e59def0d434de384b3e871c66a6ad43a02178f3d7140").expect("Bytes32 from UTXO should be valid"),
hex::decode("004005209f1f0b92d362f5d4f3b1cceab1a7b6e0d2e55e2c38e8010000000000000000000f4e65ca15db0700809fa074c96785754236d3429e2fcf909a0a0cd7e59a31e6c0ce6a6ad43a02172c31d37e").expect("Bytes32 from UTXO should be valid"),
hex::decode("0080e029852c36bc969233723d0d0c7b44c3820b1233edd53c0001000000000000000000b068c891e2d2e65e1d48a74cb19d5e2aba449403d0cc021cf49cec0175226499d5ce6a6ad43a02171413e6c3").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e00020947175283c53840a5fd95ff5fb52a69cbf049640600e00000000000000000000d4ffb8d309f3aaf7023d9e3df8587e63b672b2180b85c0d307cfbf3e334ddb1a5fd76a6ad43a0217d543d85a").expect("Bytes32 from UTXO should be valid"),
hex::decode("00801d21203bd017379dede3b30858ec121ad3afebe66093deaa00000000000000000000b15433a1853ef7eab266109f32e22af002690eea752fa0afcfd517c8c51957ed5ad76a6ad43a0217e750210f").expect("Bytes32 from UTXO should be valid"),
hex::decode("0040032083cc6f37b94764a79231d6aff125a14f964aa83aef2300000000000000000000605c53cb797ad5e44717e9345170a30375813712b4374440b7b2db0c916fcce34ddd6a6ad43a0217ac7dca95").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e00020d8f1948230933cf727b9d64f1e5786939d595727ca0c0000000000000000000064559da1901f51ddb2307790cd717cdb08421a0d596a119ea26c795695e91fb60ade6a6ad43a0217252accd0").expect("Bytes32 from UTXO should be valid"),
hex::decode("0060e524ba12f94ea5504e31ef86e8cfaacd1dfde5f2981a0a1202000000000000000000b7768b6df326e64a6091b06c253adf3ad85d58b6900c9f0262ceeff036c65ae969df6a6ad43a02177e5d71b9").expect("Bytes32 from UTXO should be valid"),
hex::decode("00200a2052a2caca7cbb37e81cafc0d37c29bb530ac9bbc42cc701000000000000000000d2b7cf8533ff1f8f67aa7a9278cea1885a1404465b8d42beee3c22572887406e7be36a6ad43a021723dfac06").expect("Bytes32 from UTXO should be valid"),
hex::decode("000000220550058d8ed56d8640ee35b6684020f73610626342c900000000000000000000adfcd1ef1d32d7394be8a552ced86e00d4a1fecd68c78e047ea79bf6fc002b7a91e46a6ad43a0217ca6118f3").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0c02658c8f4dfd9d02cccdc514498243a7fefb91f9c49069400000000000000000000a48bf36c7ff5543f078df43076242f3992e00a7cf842e80af8f3e1699acb242db3e76a6ad43a0217637c9ff1").expect("Bytes32 from UTXO should be valid"),
hex::decode("002047242bfc14c46c37991c2cef140720b1b3081ef7d87db8d3010000000000000000007f4d937fe1cfa4629e958b43d638b2079ea77e57c9cef7606f07e2fe5e888b18c4e76a6ad43a02178d9c644f").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a091278a26924cfba43b61448a7a64c5b1fee904fb8e811cd70000000000000000000008abe2adedf7edfe2fab80f34a7a930b1267f123679414348649a39ff53230808def6a6ad43a021753c783de").expect("Bytes32 from UTXO should be valid"),
hex::decode("00004830337426f9ece788d4c8241e02589ffea33f898cbe09f501000000000000000000e2e8f5511c6553157451a89067fdcc790097d030bb4bc0690bc17014eef5d2c990f16a6ad43a0217a872716e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e02920546922cd52e7f92111d725d812c1532308eb59fa22ee00000000000000000000fe634310b53673fc6c018af12c64c19060157b1fc3b73d5a5c9caa03cf3565f2baf56a6ad43a0217c76fd370").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0c121fa38469efe9223b571f74e760c96df203d80b08f3b5600000000000000000000f33c1eea09c6d87bbc4d1d2321270c21454495f21ff525534f4e20c85138a592c1f56a6ad43a021792df7b16").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0a126df55ad5154bd8f45fbe0cdb9fc9722783ce4b7a535ac01000000000000000000c533cefab59d506f73176e595df8cea46363e9b5567dff08a163a3c465dc98d14ffc6a6ad43a0217c26d2516").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a09d2b81fd6f7a8e612d81d80d5b5d64859c1b2b2b101bd1fb00000000000000000000800989d2c31d1dc5a3308452b91b1ed40206a030839737953c9d21d6923786451efd6a6ad43a021749e04799").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0d729b34736f399b13a8a2fb7479954d43c2df771c757e23901000000000000000000f7c933bad214186ec2e49c9d1b1585937280868818f320803cf6436e09a8626973016b6ad43a02171b81e870").expect("Bytes32 from UTXO should be valid"),
hex::decode("00601521556ce1a2354410a18c80b0a67be6dc16257be28890ec010000000000000000003059a134b7eca83de9aca917fabb76ee41d97032583222d6840ef22da16b5b92dd016b6ad43a021725071a5b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00602e2064168b2c362c354444ea0828f36d7cdb3572169b615601000000000000000000a4aeaacb4c25d3a83b2f8ea594325d756ec78f290a73c4953f3cbfaff10e3a3792026b6ad43a02170bb4640f").expect("Bytes32 from UTXO should be valid"),
hex::decode("0040c82357a6c79e2fb6f4d4bf95724a4bcc29bde7b449debdec01000000000000000000fa99bac89c24d15717ad877fecfbf734eb0bc75f8fb578f9470a5db668dbb8fbc0046b6ad43a02174b869a7d").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000034395bcc21a6a2c2d3ccbc2773194d11a7ddfebf0e0919000000000000000000006f505e60f36ce9f73494bfbf0c0f058fee1e2e4822c1b0fa766fb9e3251fd9b38e056b6ad43a02172c4c0184").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800d20354b5388fb81effe12c3291a4cb50cc2adc5b98d079b00000000000000000000103e5e5b3d6d17e134cc0c76af0e91fe08d72591bf4c3ad7b6aa869d5d4cb02ba7066b6ad43a02172202f70c").expect("Bytes32 from UTXO should be valid"),
hex::decode("00200e20b3001fb8573b91195cb7b9c1de390d9991f3c07978d3010000000000000000003e6addca9c34eee9459e631d9d08e77c2de70703f102ce7903838cc77208f595c7066b6ad43a02173fa4ae35").expect("Bytes32 from UTXO should be valid"),
hex::decode("006004209e4646297e4d4fd4f60548b0616a359650c35f1de7f30000000000000000000072aff40819e1e686905c68af2e73b9b951be3736437b416986cf18fd71fb8bd95a0b6b6ad43a021729d33887").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000022ee651093128e4598ff28ca297f351401d19696638b380000000000000000000038d529507d803dcf8080875cf2258aa8a551511c24f8b8ae3fa88423b0602748920b6b6ad43a02170846a89b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00608320be57374d6179c78ada919fffb2d9f908f5e18a74ff0501000000000000000000fa2f2dfb7b4d5235da7f1029804e23edbb7d3323af88199fc0ef1b79a1753459120c6b6ad43a02179a132a56").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a0d227a580e546e615e1c532f47d89de7368c30df20e2e781b01000000000000000000e12705f05a4bbf09b87493335471db4f833e12d99fd36d76c5b3279082dd096e580c6b6ad43a02177e704ef5").expect("Bytes32 from UTXO should be valid"),
hex::decode("00600f3912f664c2673dde9ab6886a85a2c9fc4efc177a0cbf4c01000000000000000000f1a81ddd80ef01b4a480de488f15402069ae8683ecc46770878b54fb7991c1a57d0d6b6ad43a0217bb384b12").expect("Bytes32 from UTXO should be valid"),
hex::decode("00803427b9cddcc2df57f4563ae0131adb3dd35bb9917139b95400000000000000000000c0b2ec9353f60fbdf5cf7da0ab0c17a78003bce2052a412c27d12665f76cb555fa136b6ad43a0217066d21e6").expect("Bytes32 from UTXO should be valid"),
hex::decode("0040ec33bb8f6fa5e2f2ed3cf60cf91d6e79fcf7cde05b11087e01000000000000000000e6075b19a084d7c5aaa8484aeff15d8b5db4db88ad69d5ee647a3d9f778fedda61146b6ad43a0217a27b3033").expect("Bytes32 from UTXO should be valid"),
hex::decode("10a0c2227eb8240f150309fcaad8a2be205ddcbf06429a508662010000000000000000007e17cf9282b77723437c083af1aa83482a73fee35c73f110303d402d88fa8bed4c156b6ad43a02174b470b31").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e00320cb4dec5b41a12bc19c29a6d424197f86c3b2ba025e25010000000000000000009d1a60c3889690294fc993f397a4dd1df48b3f9c4bdcb41306c3dedf033e4ee8461b6b6ad43a021763b89d28").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000022052e6e25c9112220845c4c3e7fd939340336d3d33cdb600000000000000000000bfc9920255dcbba49376571e9ce3bcafc87bd36045903ecf4076480d96614b7b051d6b6ad43a02172e1a9b25").expect("Bytes32 from UTXO should be valid"),
hex::decode("00209b22982e9a7ed287539bce7c3e603687aa2d3125079fb86a010000000000000000004102e56d0ce9029acb48c28745a9dce1783074391f3e70c695afdd392f6ad6d0ef1d6b6ad43a021722025f6d").expect("Bytes32 from UTXO should be valid"),
hex::decode("10e0ff3f1bb91d2d09c8f74d7245ec18f7e5b6810a7f461dcdc900000000000000000000ac2b5e4ba6695790ec04953ef4774ece540667da058dc6c0888363c4fe922e0dc81f6b6ad43a021760ea40d6").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0f731f4aa3d9f670eaece00ac84c4c19cc4e2a26ef07851220200000000000000000060ee14b2e84e0f736907009618bc2d8296194d66c7ca0a027c702569990b49090d206b6ad43a0217eeb19de9").expect("Bytes32 from UTXO should be valid"),
hex::decode("0020f92325a882bfdca063304e330248bdc067d51df8e92bcaf800000000000000000000c753e6732da8e5c98b3376480be610cbdf3cb16d836b59ab6362d2d228efedea29206b6ad43a0217c924f2a1").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a0a220f0dc827b72a458a9ce7550312c0a8750d4f068713efa01000000000000000000cc94b5a5e75437a7c856b29b12da804ddb02fe1353bb210da2606d8180ec6fb631206b6ad43a02174e9d590a").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000082063447f3bc8b428d461b82a070be3db9410ab1fc40f24000000000000000000008dc755c30eaa2c22cc7a4a5792e3656f4bf698d38c41ebc4c85165fb8c9c6ded79206b6ad43a02173e731a1f").expect("Bytes32 from UTXO should be valid"),
hex::decode("10c05729b6ea9fbe42fb1d9405e47116cb0424329411d017e1aa0000000000000000000076a747223eaa6d45d3f84e2f6828cdc487c0dbdbad42b08a9b8fffc27665a960a7216b6ad43a02174394d82b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a08520af579ef409a189ab162c788896ad825b38faea3641c401000000000000000000dee9be72bd60eda2ab12e0217e2ca16a8bcb34c32f4972b273b77a17440d00e70a226b6ad43a0217116884ce").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a00620482fd173f0a825fc34f487502c1566555f2862f0a02801000000000000000000bb5482aae60bfd220388693225362b18f72e82f9b5ceb9c6d2f9a61f2c28094fe0296b6ad43a0217fb407034").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c00120e0e9282069496b464ef564a9ead03d37ce8e7a9f12c701000000000000000000975e9f48c1083438f32c873d67b2fddfc3c8e130ac9fbc1a53da01c01c3d46c8992a6b6ad43a0217ed908988").expect("Bytes32 from UTXO should be valid"),
hex::decode("00200723899fc0d3576718c15a6719c4bc71a91bd3ccc96192ec0000000000000000000004fdb6ab38260ca750671d7a6637ef75a651448c82cfc14554d57e2748be2be9722e6b6ad43a02178a807416").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3fbf073df0898e0324336409d64b6b11987fd4465bd82900000000000000000000f4e8fc5a02f4d8f4311eb5fbf73592c88906b0bce6a08b6e321e6c7b19509dfe8f326b6ad43a02175cba391b").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000b2080a47bfbe3066208d4fc39e065d584930a323adb645b0100000000000000000044ad1fa9768359f8528ddcbee3b5f1c773f184fabb6bf87d883e91b0f3f15b89d8316b6ad43a0217329de815").expect("Bytes32 from UTXO should be valid"),
hex::decode("006003204682f5d50b7c8bdea389f6c43515d3459584ab50c406010000000000000000003d441907ec00061b73368da7754ad305e6fd796cb4cf9d3410d5a65927d8e6ce74356b6ad43a02174b0fcdc3").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000d3255924665d3d5fe933b16e09b06dcdd3bf7cce977223690000000000000000000024f812c8d99a9596021d3934a646bd50db9ca98931f3228c236952ac222217d3fe356b6ad43a0217effcbbff").expect("Bytes32 from UTXO should be valid"),
hex::decode("0000ff3f11bce3edc9ad7d6858a81f240977adb5517ef078ac5600000000000000000000a11d0de0146f427f426965160dbb0db66cebb6eb805bb3af11ee21924f2bdd71383c6b6ad43a0217936d08ca").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e0002007bb096f631e7168dcda31d75201201e543037a0b43300000000000000000000a2a7803b593fbcd881866db771b59f2f98f5f69600460fb6ad553867a1e00a00323e6b6ad43a02170dbeda27").expect("Bytes32 from UTXO should be valid"),
hex::decode("000000205a18258ccf12385ebd72c8f4f0aa2f5b5c5ec841ce10000000000000000000004a994b5f6c109f3cf616de3c0641711d1d4b848c7a04fe6e243765f67ec9cac875406b6ad43a02170d80ed5a").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0722b62395602530b6d60060a8fce332cd1908f21b30b8be4010000000000000000009508e51190aa63ec2298af3cb644eeb1498c0d83d67680efe30d5fd1f6a95d6d7e426b6ad43a02179c08e6e0").expect("Bytes32 from UTXO should be valid"),
hex::decode("00a02320b4b25be6c2ba4576586b90f8c4c3732648680cc5c2d901000000000000000000e5865a3d45646d51b65855b4b9a972198fca36241574a652ecfbab67b3f6256f97456b6ad43a021715c30439").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000030ee8847f7a144b68c6e1e0bef2b4183db01bf825d6d05020000000000000000009798b0f81dc791ce7b17eec0d89ef9db7a2b541e9f582fb2387fd9b642418faa6e476b6ad43a0217a06ef431").expect("Bytes32 from UTXO should be valid"),
hex::decode("00804521440b70d25839dbc3dd0bccd37ef751d50e576188eaa500000000000000000000314ff0850863609b917541ec201a10cd9f47f69cd2d1bf267cdb19fe3e91238bcc486b6ad43a02175e15bd74").expect("Bytes32 from UTXO should be valid"),
hex::decode("000065337e07a76349f99f3eaedfbd7ec9b048ae462fc439cddf0100000000000000000000268bf66e3c00ffff66a507729af9df227492b86f436331ebad0241ad055fe97b516b6ad43a021729b64713").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000022b936d35a2f4a755778159e39d6d80b5946bbf15b7c0b01000000000000000000640504d1606de9fce2fd0be5304c553b7f7092bd5f18c291e3fb1c63a973194fc3566b6ad43a021725c6c2d8").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000020301ba83277f8012e1902f97eea3e53d0515ef73319f201000000000000000000f8a415b0a937ef5175c9715ac98e3373e373afad63cbc9c8541c51119d91115fca5b6b6ad43a02176c9b53e1").expect("Bytes32 from UTXO should be valid"),
hex::decode("00800420ec8823f23684b5a607fa9d6194dc174497f8501a194301000000000000000000af001c7d8797bdc2f8f45149d1f4cd394903e12d59c1f8afbcdbdce3b9925436665d6b6ad43a021776224a13").expect("Bytes32 from UTXO should be valid"),
hex::decode("00000020465a566082cb8faeeb44e097377a9174a11b65a051ff010000000000000000000b6ef86b33a25b37856c2e7c68543a5066dbe67c2a087c8f4199eb289357b17360616b6ad43a021776fc5e72").expect("Bytes32 from UTXO should be valid"),
hex::decode("10e0a02c5d89719ab4dce3ed69c543336776072cdcca1c26514301000000000000000000e860d743e10db71b2b7c5a1fadb2cea11ab7200ba04b4b776fd55d0939dc6cbc61616b6ad43a02171151725e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00606e31bb3cc5e304822bfdf4506cc3b2e0aeb98a7b21280a0f01000000000000000000c897183f710feca8271a8709df053983a6d4489610a73d85b9f0014f58dd770262636b6ad43a0217a723ee06").expect("Bytes32 from UTXO should be valid"),
hex::decode("00c0812002e5d76425f391738eac0c614d8afc795fde8c0dbf7d010000000000000000008c6bae74a46ad9f2905e5258d4adaf61af2bcf7c87e86a22f084f11710d58e2b36646b6ad43a021795ac6c1e").expect("Bytes32 from UTXO should be valid"),
hex::decode("00601820856aed25759848cd1ad02caec25afdc0cf17e9ffa0aa01000000000000000000c17d5c0a7bf4af165552c3e91b4d98f4d2afa2938e7b1bdf0912f1e850680a1c53656b6ad43a0217193b3d87").expect("Bytes32 from UTXO should be valid"),
hex::decode("00e02c3ade01a429e0f5c328824f4ee063b98f24ff3b0dc0db08000000000000000000009d0b402f41ab9c16e08f95de962044b206413f878608abca2015ee2074713078606d6b6ad43a0217c0650150").expect("Bytes32 from UTXO should be valid"),
hex::decode("00008f2604b952dcd44dfcb61080ad77c5b626efd760e5f8ac8401000000000000000000ee81697e6cf7f41db37b63a9680906d3023be7a905785ef1b5ebd03fd22dd8bb2d6f6b6ad43a0217287aa17f").expect("Bytes32 from UTXO should be valid"),
hex::decode("008017262c315a069a8896ec04d88ef423ff5cb3845320a6f123010000000000000000002d42d9519e62f5a48d4880b0ff0cd8021a38eadd469a2f7aabd0886061026a5026706b6ad43a02172ea14dd2").expect("Bytes32 from UTXO should be valid"),
]
);
let thread_proof = ThreadProof::UtxoThreadProof(utxo_thread_proof);
let pulled_thread_key = thread_proof.get_unique_key(session);
assert!(!PulledThreads::<T>::contains_key(&pulled_thread_key));
#[extrinsic_call]
pull_thread(RawOrigin::None, network_id, session, thread_proof);
assert!(PulledThreads::<T>::contains_key(&pulled_thread_key));
Ok(())
}
#[benchmark]
fn pull_evm_thread() -> Result<(), BenchmarkError> {
let session = 69;
let (network_id, _, _) = prepare_pallet::<T>(1);
// https://sepolia.etherscan.io/address/0x9cffbdbdf29c67c5dbab1b5e8ae897aeafcaf70c#readContract
// getRoot: session 0, atBlock 11383380
let root_string =
hex::decode("f72b9ef0b66c8cc3952ffd11dc0b85f3b02e6edfa9236da400138f2fcba4668d")
.expect("EVM Root hex should be valid");
let expected_root = H256::from_slice(&root_string);
LoomStates::<T>::insert(&network_id, &session, expected_root);
let receiver_32: [u8; 32] =
hex::decode("0ae3ba32f8555fe3031461e2cb6feeadb67029ef7490b982824fc90751a4cb3e")
.expect("Bytes32 from EVM should be valid")
.try_into()
.expect("Hex string must be exactly 32 bytes long");
let receiver_account = sp_runtime::AccountId32::from(receiver_32);
let transfer_amount: BalanceOf<T> = 420u64.unique_saturated_into();
let proof = vec![
H256::from_slice(
&hex::decode("6dfb19f049f557abff273ce9efb9195924d41e5699b1b7755be2c515b25ef7b6")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("9e0f9c5b8ffa146b74bb614482c0bc65a1ef961a8fa9a3a031a36c97f8c70733")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("ecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("defff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7")
.expect("Proof should be valid"),
),
H256::from_slice(
&hex::decode("292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead")
.expect("Proof should be valid"),
),
];
let slots = vec![H256::from_slice(
&hex::decode("2e474dabf492786b3505d7b7cf0bf07c9dbce92c91982fa7344c77e24fd4310d")
.expect("Slot should be valid hex"),
)];
let payload = ThreadPayload::new(1, transfer_amount, receiver_account);
let evm_thread_proof = EvmThreadProof::new(0, payload, proof, slots);
let thread_proof = ThreadProof::EvmThreadProof(evm_thread_proof);
let pulled_thread_key = thread_proof.get_unique_key(session);
assert!(!PulledThreads::<T>::contains_key(&pulled_thread_key));
#[extrinsic_call]
pull_thread(RawOrigin::None, network_id, session, thread_proof);
assert!(PulledThreads::<T>::contains_key(&pulled_thread_key));
Ok(())
}
impl_benchmark_test_suite!(
Pallet,
crate::mock::new_test_ext(vec![], vec![]),
crate::mock::TestRuntime
);
}

View File

@ -1,96 +0,0 @@
use crate::ExternalBlockNumber;
pub enum WeavingError<NetworkId> {
DeadlineReached,
NoStoredNetworks,
UnparsableRequestBody,
UnknownWeavingPhase(NetworkId),
OffchainLockPeriod(NetworkId),
NoEndpointsAvailable(NetworkId),
ContradictoryHashes(NetworkId),
ContradictoryBlocks(u32, ExternalBlockNumber, NetworkId),
}
impl<NetworkId: core::fmt::Debug> core::fmt::Debug for WeavingError<NetworkId> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Self::NoStoredNetworks => write!(fmt, "No stored networks to be watch on."),
Self::UnparsableRequestBody => {
write!(fmt, "HTTP request body contains invalid UTF-8 sequences.")
}
Self::DeadlineReached => {
write!(fmt, "The request deadline or network timeout was hit.")
}
Self::UnknownWeavingPhase(network_id) => {
write!(fmt, "Unknown weaving phase for network: {:?}.", network_id)
}
Self::OffchainLockPeriod(network_id) => write!(
fmt,
"Storage lock is still held for network: {:?}.",
network_id
),
Self::NoEndpointsAvailable(network_id) => write!(
fmt,
"Zero RPC URLs configured for network: {:?}.",
network_id
),
Self::ContradictoryHashes(network_id) => write!(
fmt,
"Divergence among reported hashes for network: {:?}.",
network_id
),
Self::ContradictoryBlocks(len, deviation, network_id) => {
write!(fmt, "Divergence among {len} reported blocks exceeds the maximum threshold of {deviation} provided for network: {:?}.", network_id)
}
}
}
}
pub enum RpcResolverError {
JsonDeserializationFailed,
UnparsableResponseBody,
RpcEmptyResultField,
UnknownNetworkType,
InvalidResponseId,
RpcProtocolError,
DeadlineReached,
Non200HttpCode,
IoError,
Unknown,
}
impl core::fmt::Debug for RpcResolverError {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Self::JsonDeserializationFailed => {
write!(fmt, "Failed to deserialize JSON payload into target type.")
}
Self::UnparsableResponseBody => {
write!(fmt, "HTTP response body contains invalid UTF-8 sequences.")
}
Self::RpcEmptyResultField => write!(
fmt,
"The 'result' field in the JSON-RPC response is missing or null."
),
Self::UnknownNetworkType => {
write!(fmt, "he provided network type identifier is not supported.")
}
Self::RpcProtocolError => write!(
fmt,
"The server returned a protocol-level JSON-RPC error block."
),
Self::IoError => write!(
fmt,
"An internal input/output or stream processing error occurred."
),
Self::Non200HttpCode => {
write!(fmt, "Server responded with a non-200 OK HTTP status code.")
}
Self::DeadlineReached => {
write!(fmt, "The request deadline or network timeout was hit.")
}
Self::InvalidResponseId => write!(fmt, "Response ID does not match request ID."),
Self::Unknown => write!(fmt, "An unclassified or generic error occurred."),
}
}
}

View File

@ -1,412 +0,0 @@
use ghost_traits::networks::NetworkRpcResolver;
use serde::{Deserialize, Deserializer};
use sp_std::vec;
use serde_json::json;
use sp_std::vec::Vec;
use sp_core::{H256, U256};
use sp_runtime::offchain::http::{Error as HttpError, HttpResult, PendingRequest};
use crate::{
ExternalBlockNumber, NetworkType, RequestId, RpcResolverError, WeavingError, WeavingSession,
LOG_TARGET,
};
fn parse_response(
http_result: HttpResult,
request_id: RequestId,
network_type: &NetworkType,
) -> Result<NetworkResponseType, RpcResolverError> {
let response = http_result.map_err(|err| match err {
HttpError::DeadlineReached => RpcResolverError::DeadlineReached,
HttpError::IoError => RpcResolverError::IoError,
HttpError::Unknown => RpcResolverError::Unknown,
})?;
if response.code != 200 {
return Err(RpcResolverError::Non200HttpCode);
}
let response_body = response.body().collect::<Vec<u8>>();
let response_str = sp_std::str::from_utf8(&response_body)
.map_err(|_| RpcResolverError::UnparsableResponseBody)?;
let response_data: NetworkResponse = match network_type {
NetworkType::Evm => {
let evm_response: EvmResponse = serde_json::from_str(response_str)
.map_err(|_| RpcResolverError::JsonDeserializationFailed)?;
evm_response.into()
}
NetworkType::Utxo => {
let utxo_response: UtxoResponse = serde_json::from_str(response_str)
.map_err(|_| RpcResolverError::JsonDeserializationFailed)?;
utxo_response.into()
}
_ => return Err(RpcResolverError::UnknownNetworkType),
};
if request_id != response_data.id {
return Err(RpcResolverError::InvalidResponseId);
}
if response_data.error.is_some() {
return Err(RpcResolverError::RpcProtocolError);
}
response_data
.result
.ok_or(RpcResolverError::RpcEmptyResultField)
}
fn parse_pending_requests<'a, R: Copy + Into<RequestId> + 'static>(
pending_requests: Vec<PendingRequest>,
deadline: sp_runtime::offchain::Timestamp,
request_id: R,
network_type: &'a NetworkType,
) -> impl Iterator<Item = NetworkResponseType> + 'a {
PendingRequest::try_wait_all(pending_requests, Some(deadline))
.into_iter()
.filter_map(move |wait_result| {
let http_result = wait_result
.inspect_err(|_| {
log::error!(
target: LOG_TARGET,
"🕸️ Error during request preparation: {:?}",
WeavingError::<u32>::DeadlineReached,
)
})
.ok()?;
parse_response(http_result, request_id.into(), network_type)
.inspect_err(|err| {
log::error!(
target: LOG_TARGET,
"🕸️ Error during response parsing: {:?}",
err,
)
})
.ok()
})
}
impl NetworkRpcResolver<WeavingError<u32>, ExternalBlockNumber, H256, RequestId, WeavingSession>
for NetworkType
{
fn get_block_request_body(&self, id: u64) -> Vec<u8> {
let method = match self {
NetworkType::Evm => "eth_blockNumber",
NetworkType::Utxo => "getblockcount",
_ => "unknown",
};
serde_json::to_vec(&json!({"id": id, "jsonrpc": "2.0", "method": method}))
.unwrap_or_else(|_| b"{}".to_vec())
}
fn get_hash_request_body(
&self,
id: u64,
target_block: ExternalBlockNumber,
weaving_session: WeavingSession,
contract_ref: &[u8],
selector_ref: &[u8],
) -> Vec<u8> {
extern crate alloc;
let (method, params) = match self {
NetworkType::Evm => {
let contract_hex = hex::encode(contract_ref);
let selector_hex = hex::encode(selector_ref);
let encoded_session = alloc::format!("{:064x}", weaving_session);
let encoded_block = alloc::format!("{:064x}", target_block);
let to_field = alloc::format!("0x{}", contract_hex);
let data_field =
alloc::format!("0x{}{}{}", selector_hex, encoded_session, encoded_block,);
(
"eth_call",
json!([{"to": to_field, "data": data_field}, "latest"]),
)
}
NetworkType::Utxo => ("getblockhash", json!([target_block])),
_ => ("unknown", json!([])),
};
serde_json::to_vec(&json!({"id": id, "jsonrpc": "2.0", "method": method, "params": params}))
.unwrap_or_else(|_| b"{}".to_vec())
}
fn parse_block_requests(
&self,
pending_requests: Vec<PendingRequest>,
deadline: sp_runtime::offchain::Timestamp,
request_id: RequestId,
) -> Vec<ExternalBlockNumber> {
parse_pending_requests(pending_requests, deadline, request_id, self)
.filter_map(|response| match response {
NetworkResponseType::BlockNumber(block) => Some(block),
NetworkResponseType::TreeRoot(_, _) => None,
})
.collect()
}
fn parse_hash_requests(
&self,
pending_requests: Vec<PendingRequest>,
deadline: sp_runtime::offchain::Timestamp,
request_id: RequestId,
) -> Vec<(H256, WeavingSession)> {
parse_pending_requests(pending_requests, deadline, request_id, self)
.filter_map(|response| match response {
NetworkResponseType::TreeRoot(hash, next) => Some((hash, next)),
NetworkResponseType::BlockNumber(_) => None,
})
.collect()
}
}
#[derive(Clone, PartialEq, Deserialize)]
struct NetworkResponse {
pub id: RequestId,
pub error: Option<serde_json::Value>,
pub result: Option<NetworkResponseType>,
}
impl From<EvmResponse> for NetworkResponse {
fn from(evm_response: EvmResponse) -> Self {
Self {
id: evm_response.id,
error: evm_response.error,
result: evm_response.result.map(Into::into),
}
}
}
impl From<UtxoResponse> for NetworkResponse {
fn from(utxo_response: UtxoResponse) -> Self {
Self {
id: utxo_response.id,
error: utxo_response.error,
result: utxo_response.result.map(Into::into),
}
}
}
#[derive(Clone, PartialEq, Deserialize)]
enum NetworkResponseType {
BlockNumber(ExternalBlockNumber),
TreeRoot(H256, WeavingSession),
}
impl From<EvmResponseType> for NetworkResponseType {
fn from(evm_type: EvmResponseType) -> Self {
match evm_type {
EvmResponseType::BlockNumber(block) => Self::BlockNumber(block),
EvmResponseType::TreeRoot(root, next) => Self::TreeRoot(root, next),
}
}
}
impl From<UtxoResponseType> for NetworkResponseType {
fn from(utxo_type: UtxoResponseType) -> Self {
match utxo_type {
UtxoResponseType::BlockNumber(block) => Self::BlockNumber(block),
UtxoResponseType::TreeRoot(root) => Self::TreeRoot(root, WeavingSession::MAX),
}
}
}
#[derive(Deserialize, Eq, PartialEq, Clone)]
struct EvmResponse {
pub id: RequestId,
pub error: Option<serde_json::Value>,
pub result: Option<EvmResponseType>,
}
#[derive(Deserialize, Eq, PartialEq, Clone)]
struct UtxoResponse {
pub id: RequestId,
pub error: Option<serde_json::Value>,
pub result: Option<UtxoResponseType>,
}
#[derive(Deserialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
enum EvmResponseType {
#[serde(deserialize_with = "de_hex_string_to_block_number")]
BlockNumber(ExternalBlockNumber),
#[serde(deserialize_with = "de_contract_tuple")]
TreeRoot(H256, WeavingSession),
}
#[derive(Deserialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
enum UtxoResponseType {
BlockNumber(ExternalBlockNumber),
#[serde(deserialize_with = "de_string_to_h256")]
TreeRoot(H256),
}
fn de_hex_string_to_block_number<'de, D>(de: D) -> Result<ExternalBlockNumber, D::Error>
where
D: Deserializer<'de>,
{
let s: &'de str = Deserialize::deserialize(de)?;
let clean_hex = s.strip_prefix("0x").unwrap_or(s);
let trimmed_hex = clean_hex.trim_start_matches('0');
if trimmed_hex.is_empty() {
return Ok(Default::default());
}
ExternalBlockNumber::from_str_radix(trimmed_hex, 16)
.map_err(|_| serde::de::Error::custom("Failed to parse block number hex"))
}
fn de_string_to_h256<'de, D>(de: D) -> Result<H256, D::Error>
where
D: Deserializer<'de>,
{
let s: &'de str = Deserialize::deserialize(de)?;
let clean_hex = s.strip_prefix("0x").unwrap_or(s);
let mut bytes = [0u8; 32];
hex::decode_to_slice(clean_hex, &mut bytes)
.map_err(|_| serde::de::Error::custom("Failed to parse hex string into H256"))?;
Ok(H256::from(bytes))
}
fn de_contract_tuple<'de, D>(de: D) -> Result<(H256, WeavingSession), D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(de)?;
let clean_hex = s.strip_prefix("0x").unwrap_or(s);
if clean_hex.len() != 128 {
return Err(serde::de::Error::custom(
"Invalid hex string length for (bytes32, uint256)",
));
}
let (bytes32_hex, uint256_hex) = clean_hex.split_at(64);
let mut bytes = [0u8; 32];
hex::decode_to_slice(bytes32_hex, &mut bytes)
.map_err(|_| serde::de::Error::custom("Failed to parse hex string into H256"))?;
let root_hash = H256::from(bytes);
let uint256_val = U256::from_str_radix(uint256_hex, 16)
.map_err(|_| serde::de::Error::custom("Failed to parse uint256"))?;
let next_weaving_session: WeavingSession = uint256_val
.try_into()
.map_err(|_| serde::de::Error::custom("EVM uint256 value overflow bounds"))?;
Ok((root_hash, next_weaving_session))
}
#[cfg(test)]
mod parsing_tests {
use super::*;
use serde::Deserialize;
use sp_core::H256;
#[derive(Deserialize)]
struct TestBlock {
#[serde(deserialize_with = "de_hex_string_to_block_number")]
val: ExternalBlockNumber,
}
#[derive(Deserialize)]
struct TestHash {
#[serde(deserialize_with = "de_string_to_h256")]
val: H256,
}
#[derive(Deserialize)]
struct TestTuple {
#[serde(deserialize_with = "de_contract_tuple")]
val: (H256, WeavingSession),
}
#[test]
fn test_de_hex_string_to_block_number_success() {
let json = r#"{"val": "0x64"}"#;
let parsed: TestBlock = serde_json::from_str(json).unwrap();
assert_eq!(parsed.val, 100);
let json_zeros = r#"{"val": "0x00000a"}"#;
let parsed_zeros: TestBlock = serde_json::from_str(json_zeros).unwrap();
assert_eq!(parsed_zeros.val, 10);
let json_zero = r#"{"val": "0x0"}"#;
let parsed_zero: TestBlock = serde_json::from_str(json_zero).unwrap();
assert_eq!(parsed_zero.val, 0);
}
#[test]
fn test_de_hex_string_to_block_number_invalid() {
let json = r#"{"val": "0x6G"}"#;
let result: Result<TestBlock, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_de_string_to_h256_success() {
let raw_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let json = format!(r#"{{"val": "0x{}"}}"#, raw_hash);
let parsed: TestHash = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.val, H256::repeat_byte(0xAA));
}
#[test]
fn test_de_string_to_h256_invalid_length() {
let json = r#"{"val": "0x00aa"}"#;
let result: Result<TestHash, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_de_contract_tuple_success() {
let hash_part = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let session_part = "000000000000000000000000000000000000000000000000000000000000002a";
let json = format!(r#"{{"val": "0x{}{}"}}"#, hash_part, session_part);
let parsed: TestTuple = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.val.0, H256::repeat_byte(0xBB));
assert_eq!(parsed.val.1, 42u64);
}
#[test]
fn test_de_contract_tuple_invalid_length() {
let json = r#"{"val": "0xbbbb002a"}"#;
let result: Result<TestTuple, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.err().unwrap().to_string();
assert!(err_msg.contains("Invalid hex string length"));
}
#[test]
fn test_de_contract_tuple_overflow() {
let hash_part = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let session_overflow = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
let json = format!(r#"{{"val": "0x{}{}"}}"#, hash_part, session_overflow);
let result: Result<TestTuple, _> = serde_json::from_str(&json);
assert!(result.is_err());
let err_msg = result.err().unwrap().to_string();
assert!(err_msg.contains("EVM uint256 value overflow bounds"));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,171 +0,0 @@
#![cfg(any(test, feature = "runtime-benchmarks"))]
use crate::{self as ghost_weaver, *};
use frame_support::{
derive_impl, parameter_types,
traits::{ConstU32, ConstU64, Everything},
};
use frame_system::EnsureRoot;
use sp_core::{Pair, H256};
use sp_runtime::{
testing::TestXt,
traits::{BlakeTwo256, IdentityLookup},
AccountId32, BuildStorage,
};
use ghost_helpers::networks::NetworkInitiationBuilder;
type Block = frame_system::mocking::MockBlock<TestRuntime>;
type Balance = u64;
type NetworkId = u32;
frame_support::construct_runtime!(
pub enum TestRuntime {
System: frame_system,
Balances: pallet_balances,
GhostNetworks: ghost_networks,
GhostWeaver: ghost_weaver,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for TestRuntime {
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId32;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
impl pallet_balances::Config for TestRuntime {
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<1>;
type AccountStore = System;
type WeightInfo = ();
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
}
parameter_types! {
pub const MaxNetworks: u32 = 5;
}
impl ghost_networks::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type NetworkId = NetworkId;
type RegisterOrigin = EnsureRoot<Self::AccountId>;
type UpdateOrigin = EnsureRoot<Self::AccountId>;
type RemoveOrigin = EnsureRoot<Self::AccountId>;
type MaxNetworks = MaxNetworks;
type WeightInfo = ();
}
#[allow(dead_code)]
pub type Extrinsic = TestXt<RuntimeCall, ()>;
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for TestRuntime
where
RuntimeCall: From<LocalCall>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = TestXt<RuntimeCall, ()>;
}
parameter_types! {
pub const UnsignedPriority: u64 = 100;
pub const WeavingDelay: u64 = 10;
pub const AttestationDelay: u64 = 3;
}
impl ghost_weaver::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type AuthorityId = ghost_weaver::sr25519::AuthorityId;
type Currency = Balances;
type NetworkDataHandler = GhostNetworks;
type BlockNumberProvider = System;
type MaxAuthorities = ConstU32<20>;
type MaxAuthoritiesChunks = ConstU32<1>;
type WeavingDelay = WeavingDelay;
type AttestationDelay = AttestationDelay;
type UnsignedPriority = UnsignedPriority;
type WeightInfo = ();
}
#[allow(dead_code)]
pub fn new_test_ext(
authorities: Vec<sr25519::AuthorityPair>,
networks_raw: Vec<(NetworkId, NetworkData, Balance)>,
) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<TestRuntime>::default()
.build_storage()
.unwrap();
let genesis_authorities = authorities
.iter()
.enumerate()
.map(|(i, pair)| {
let mut id_bytes = [0u8; 32];
let id_u64 = (i + 1) as u64;
id_bytes[0..8].copy_from_slice(&id_u64.to_le_bytes());
let account_id = AccountId32::from(id_bytes);
(account_id, pair.public().clone())
})
.collect::<Vec<_>>();
let networks = networks_raw
.into_iter()
.map(|(network_id, network_data, amount)| {
NetworkInitiationBuilder::default()
.with_network_id(network_id)
.with_network_data(network_data)
.with_gatekeeper_amount(amount)
.build()
})
.collect::<Vec<_>>();
ghost_networks::GenesisConfig::<TestRuntime> { networks }
.assimilate_storage(&mut t)
.unwrap();
ghost_weaver::GenesisConfig::<TestRuntime> {
authorities: genesis_authorities,
loom_states: vec![],
}
.assimilate_storage(&mut t)
.unwrap();
let keystore = sp_keystore::testing::MemoryKeystore::new();
let mut ext = sp_io::TestExternalities::new(t);
ext.register_extension(sp_keystore::KeystoreExt::new(keystore));
ext.execute_with(|| System::set_block_number(1));
ext
}

File diff suppressed because it is too large Load Diff

View File

@ -1,598 +0,0 @@
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::{pallet_prelude::ConstU32, BoundedVec};
use scale_info::TypeInfo;
use sp_core::{H256, U256};
use sp_std::vec::Vec;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, Saturating, UniqueSaturatedInto},
AccountId32, RuntimeDebug,
};
use ghost_helpers::{
hash_chain::{cumulative_hash, sequential_hash, verify_hash_ancestry},
merkle_tree::verify_tree_proof,
SubstrateBlake2Hasher, SubstrateKeccakHasher, UtxoSha2Hasher,
};
use ghost_traits::hashing::GhostHasher;
use crate::{ExternalBlockNumber, ThreadId, WeavingSession};
const EVM_MERKLE_DEPTH: u32 = 8;
const EVM_VALUES_DEPTH: u32 = 8;
const UTXO_MAX_PROOF_BYTES: u32 = 1024;
const UTXO_MAX_HEADER_BYTES: u32 = 80;
const UTXO_MAX_HISTORY_DEPTH: u32 = 144;
const UTXO_MAX_TX_BYTES: u32 = ghost_helpers::utxo::MAX_TX_LEN as u32;
pub enum WeavingOk<NetworkId> {
Block(ExternalBlockNumber, NetworkId),
Hash(ExternalBlockNumber, H256, WeavingSession, NetworkId),
}
impl<NetworkId> core::fmt::Display for WeavingOk<NetworkId>
where
NetworkId: core::fmt::Debug,
{
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
Self::Block(block, network_id) => write!(fmt, "Verified block height #{:?} found for network: {:?}.", block, network_id),
Self::Hash(block, hash, next_session, network_id) => write!(fmt, "Extracted Merkle Root {:?} where next session is {:?}; from block #{:?} for network: {:?}.", hash, next_session, block, network_id),
}
}
}
#[derive(
Default,
RuntimeDebug,
Clone,
Copy,
Eq,
PartialEq,
Ord,
PartialOrd,
Encode,
Decode,
TypeInfo,
MaxEncodedLen,
)]
pub struct WeavingState<AuthIndex> {
pub hash: H256,
pub count: AuthIndex,
pub next_session: WeavingSession,
}
impl<AuthIndex> WeavingState<AuthIndex> {
pub fn new(hash: H256, next_session: WeavingSession, count: AuthIndex) -> Self {
Self {
hash,
next_session,
count,
}
}
}
#[derive(
Default,
RuntimeDebug,
Clone,
Copy,
Eq,
PartialEq,
Ord,
PartialOrd,
Encode,
Decode,
TypeInfo,
MaxEncodedLen,
)]
pub struct AttestationContext<Block, NetworkId, AuthIndex> {
block: Block,
network_id: NetworkId,
authority_index: AuthIndex,
weaving_session: WeavingSession,
}
pub trait AttestationMetadata<Block, NetworkId, AuthIndex> {
fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex>;
fn block(&self) -> Block
where
Block: Copy,
{
self.context().block
}
fn network_id(&self) -> NetworkId
where
NetworkId: Copy,
{
self.context().network_id
}
fn authority_index(&self) -> AuthIndex
where
AuthIndex: Copy,
{
self.context().authority_index
}
fn weaving_session(&self) -> WeavingSession {
self.context().weaving_session
}
}
impl<Block, NetworkId, AuthIndex> AttestationContext<Block, NetworkId, AuthIndex> {
pub fn with_current_block(mut self, block: Block) -> Self {
self.block = block;
self
}
pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
self.network_id = network_id;
self
}
pub fn with_authority_index(mut self, authority_index: AuthIndex) -> Self {
self.authority_index = authority_index;
self
}
pub fn with_weaving_session(mut self, weaving_session: WeavingSession) -> Self {
self.weaving_session = weaving_session;
self
}
pub fn build_block_attestation(
self,
external_block: ExternalBlockNumber,
) -> BlockAttestation<Block, NetworkId, AuthIndex> {
BlockAttestation::<Block, NetworkId, AuthIndex> {
context: self,
external_block,
}
}
pub fn build_hash_attestation(
self,
root_hash: H256,
next_session: WeavingSession,
) -> HashAttestation<Block, NetworkId, AuthIndex> {
HashAttestation::<Block, NetworkId, AuthIndex> {
root_hash,
next_session,
context: self,
}
}
}
#[derive(
RuntimeDebug,
Clone,
Copy,
Eq,
PartialEq,
Ord,
PartialOrd,
Encode,
Decode,
TypeInfo,
MaxEncodedLen,
)]
pub struct BlockAttestation<Block, NetworkId, AuthIndex> {
context: AttestationContext<Block, NetworkId, AuthIndex>,
pub external_block: ExternalBlockNumber,
}
#[derive(
RuntimeDebug,
Clone,
Copy,
Eq,
PartialEq,
Ord,
PartialOrd,
Encode,
Decode,
TypeInfo,
MaxEncodedLen,
)]
pub struct HashAttestation<Block, NetworkId, AuthIndex> {
context: AttestationContext<Block, NetworkId, AuthIndex>,
pub next_session: WeavingSession,
pub root_hash: H256,
}
impl<Block, NetworkId, AuthIndex> AttestationMetadata<Block, NetworkId, AuthIndex>
for BlockAttestation<Block, NetworkId, AuthIndex>
{
fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex> {
&self.context
}
}
impl<Block, NetworkId, AuthIndex> AttestationMetadata<Block, NetworkId, AuthIndex>
for HashAttestation<Block, NetworkId, AuthIndex>
{
fn context(&self) -> &AttestationContext<Block, NetworkId, AuthIndex> {
&self.context
}
}
#[derive(Default, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct NetworkAttestation<Block, BoundedMap>
where
Block: AtLeast32BitUnsigned + Encode + Decode + TypeInfo + MaxEncodedLen + Default,
BoundedMap: Encode + Decode + TypeInfo + MaxEncodedLen + Default + Clone,
{
pub until: Block,
pub map: BoundedMap,
}
#[derive(Default, Copy, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct TapestryDraft<Block: AtLeast32BitUnsigned> {
pub until: Block,
pub median: ExternalBlockNumber,
}
#[derive(Default, Clone, Copy)]
pub struct TapestryDraftBuilder<Block, Delay>
where
Block: Saturating + AtLeast32BitUnsigned + Copy,
Delay: UniqueSaturatedInto<Block> + AtLeast32BitUnsigned + Copy,
{
delay: Delay,
block: Block,
median: ExternalBlockNumber,
}
impl<Block, Delay> TapestryDraftBuilder<Block, Delay>
where
Block: Saturating + AtLeast32BitUnsigned + Copy,
Delay: UniqueSaturatedInto<Block> + AtLeast32BitUnsigned + Copy,
{
pub fn with_block_delay(mut self, delay: Delay) -> Self {
self.delay = delay;
self
}
pub fn with_current_block(mut self, block: Block) -> Self {
self.block = block;
self
}
pub fn with_median_external(mut self, median: ExternalBlockNumber) -> Self {
self.median = median;
self
}
pub fn build(self) -> TapestryDraft<Block> {
let delay_converted: Block = self.delay.unique_saturated_into();
let until = self.block.saturating_add(delay_converted);
TapestryDraft {
until,
median: self.median,
}
}
}
#[derive(Default, Copy, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct LoomState {
pub hash: H256,
pub pulled_threads: ThreadId,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ThreadPayload<Balance: UniqueSaturatedInto<u128>> {
proof_index: ThreadId,
amount: Balance,
receiver: AccountId32,
}
impl<Balance> ThreadPayload<Balance>
where
Balance: UniqueSaturatedInto<u128>,
{
pub fn new(proof_index: ThreadId, amount: Balance, receiver: AccountId32) -> Self {
Self {
proof_index,
amount,
receiver,
}
}
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum ThreadProof<Balance: UniqueSaturatedInto<u128>> {
EvmThreadProof(EvmThreadProof<Balance>),
UtxoThreadProof(UtxoThreadProof<Balance>),
}
impl<Balance> ThreadProof<Balance>
where
Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
pub fn is_evm_proof_correct(&self) -> bool {
match self {
Self::EvmThreadProof(_) => true,
Self::UtxoThreadProof(_) => false,
}
}
pub fn is_utxo_proof_correct(&self) -> bool {
match self {
Self::UtxoThreadProof(_) => true,
Self::EvmThreadProof(_) => false,
}
}
pub fn amount(&self) -> Balance {
match self {
Self::UtxoThreadProof(utxo_thread) => utxo_thread.amount(),
Self::EvmThreadProof(evm_thread) => evm_thread.amount(),
}
}
pub fn proof_index(&self) -> ThreadId {
match self {
Self::UtxoThreadProof(utxo_thread) => utxo_thread.proof_index(),
Self::EvmThreadProof(evm_thread) => evm_thread.proof_index(),
}
}
pub fn verify_proof(&self, root_hash: H256, gatekeeper: &[u8]) -> Option<AccountId32> {
match self {
Self::UtxoThreadProof(utxo_thread) => utxo_thread.verify_proof(root_hash, gatekeeper),
Self::EvmThreadProof(evm_thread) => evm_thread.verify_proof(root_hash),
}
}
pub fn get_unique_key(&self, session: WeavingSession) -> H256 {
match self {
Self::EvmThreadProof(thread_info) => thread_info.get_unique_key(session),
Self::UtxoThreadProof(utxo_thread) => utxo_thread.get_unique_key(),
}
}
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct EvmThreadProof<Balance: UniqueSaturatedInto<u128>> {
slot_index: ThreadId,
payload: ThreadPayload<Balance>,
proof: BoundedVec<H256, ConstU32<EVM_MERKLE_DEPTH>>,
slots: BoundedVec<H256, ConstU32<EVM_VALUES_DEPTH>>,
}
impl<Balance> EvmThreadProof<Balance>
where
Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
pub fn amount(&self) -> Balance {
self.payload.amount.clone()
}
pub fn proof_index(&self) -> ThreadId {
self.payload.proof_index
}
pub fn verify_proof(&self, root_hash: H256) -> Option<AccountId32> {
if EVM_MERKLE_DEPTH as usize != self.proof.len() {
return None;
}
let proof_valid = verify_tree_proof::<SubstrateKeccakHasher, _>(
&self.get_preimage_slice().to_fixed_bytes(),
self.proof.as_ref(),
root_hash,
self.payload.proof_index,
);
if !proof_valid {
return None;
}
Some(self.payload.receiver.clone())
}
fn get_preimage_slice(&self) -> H256 {
let slot_index_usize: usize = self.slot_index.unique_saturated_into();
(slot_index_usize < self.slots.len())
.then(|| {
self.slots
.iter()
.enumerate()
.map(|(index, &preimage)| {
if index == slot_index_usize {
self.compute_arguments_hash()
} else {
preimage
}
})
.collect::<Vec<_>>()
})
.map(|slots| cumulative_hash::<SubstrateKeccakHasher, _>(slots))
.unwrap_or(SubstrateKeccakHasher::empty())
}
pub fn get_unique_key(&self, session: WeavingSession) -> H256 {
let session_bytes = (session as u64).to_be_bytes();
let slot_bytes = (self.slot_index as u64).to_be_bytes();
let proof_bytes = (self.payload.proof_index as u64).to_be_bytes();
let mut h1 = [0u8; 32]; h1[24..32].copy_from_slice(&session_bytes);
let mut h2 = [0u8; 32]; h2[24..32].copy_from_slice(&slot_bytes);
let mut h3 = [0u8; 32]; h3[24..32].copy_from_slice(&proof_bytes);
let metadata: [H256; 3] = [H256::from(h1), H256::from(h2), H256::from(h3)];
sequential_hash::<SubstrateBlake2Hasher, _>(metadata)
}
pub fn new(
slot_index: ThreadId,
payload: ThreadPayload<Balance>,
proof_vec: Vec<H256>,
slots_vec: Vec<H256>,
) -> Self {
let proof = BoundedVec::<H256, ConstU32<EVM_MERKLE_DEPTH>>::try_from(proof_vec)
.expect("Should be valid EVM proof");
let slots = BoundedVec::<H256, ConstU32<EVM_MERKLE_DEPTH>>::try_from(slots_vec)
.expect("Should be valud EVM proof");
Self {
slot_index,
payload,
proof,
slots,
}
}
fn compute_arguments_hash(&self) -> H256 {
let amount_u128: u128 = self.amount().unique_saturated_into();
let amount_u256 = U256::from(amount_u128);
let slot_index_u64: u64 = self.slot_index.unique_saturated_into();
let slot_index_u256 = U256::from(slot_index_u64);
let mut padded_amount = [0u8; 32];
amount_u256.to_big_endian(&mut padded_amount);
let mut padded_slot_index = [0u8; 32];
slot_index_u256.to_big_endian(&mut padded_slot_index);
let inputs: [&[u8]; 3] = [
padded_amount.as_ref(),
self.payload.receiver.as_ref(),
padded_slot_index.as_ref(),
];
sequential_hash::<SubstrateKeccakHasher, _>(inputs)
}
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct UtxoThreadProof<Balance: UniqueSaturatedInto<u128>> {
o_index: ThreadId,
w_index: ThreadId,
expected_amount: Balance,
payload: ThreadPayload<Balance>,
gettxproof_bytes: BoundedVec<u8, ConstU32<UTXO_MAX_PROOF_BYTES>>,
getrawtransaction_bytes: BoundedVec<u8, ConstU32<UTXO_MAX_TX_BYTES>>,
ancestry_headers: BoundedVec<
BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>,
ConstU32<UTXO_MAX_HISTORY_DEPTH>,
>,
}
impl<Balance> UtxoThreadProof<Balance>
where
Balance: Copy + Clone + Eq + PartialEq + UniqueSaturatedInto<u128>,
{
pub fn amount(&self) -> Balance {
self.expected_amount
}
pub fn receiver(&self) -> AccountId32 {
self.payload.receiver.clone()
}
pub fn proof_index(&self) -> ThreadId {
self.payload.proof_index
}
pub fn verify_proof(&self, root_hash: H256, gatekeeper_ref: &[u8]) -> Option<AccountId32> {
use ghost_helpers::utxo::{Header, Transaction};
if self.gettxproof_bytes.len() < 85 {
return None;
}
let header_bytes = &self.gettxproof_bytes[..80];
let is_chain_valid = verify_hash_ancestry::<UtxoSha2Hasher, _>(
header_bytes,
self.ancestry_headers.iter(),
root_hash,
);
if !is_chain_valid {
return None;
}
let transaction =
Transaction::<UtxoSha2Hasher>::try_from(self.getrawtransaction_bytes.as_ref()).ok()?;
let header = Header::<UtxoSha2Hasher>::try_from(self.gettxproof_bytes.as_ref()).ok()?;
let txid_preimage = transaction.compute_txid()?;
let root_hash = header.extract_proof(txid_preimage).ok()?;
if root_hash != header.merkle_root {
return None;
}
let account_id_bytes = transaction.validate(
self.o_index,
self.w_index,
gatekeeper_ref,
self.expected_amount,
)?;
Some(AccountId32::new(account_id_bytes))
}
pub fn get_unique_key(&self) -> H256 {
let o_index_bytes = (self.o_index as u64).to_be_bytes();
let mut o_index_raw = [0u8; 32];
o_index_raw[24..32].copy_from_slice(&o_index_bytes);
let o_index_hash = H256::from(o_index_raw);
let metadata: &[&[u8]] = &[
self.gettxproof_bytes.as_ref(),
self.getrawtransaction_bytes.as_ref(),
o_index_hash.as_ref(),
];
sequential_hash::<SubstrateBlake2Hasher, _>(metadata)
}
pub fn new(
o_index: ThreadId,
w_index: ThreadId,
expected_amount: Balance,
payload: ThreadPayload<Balance>,
gettxproof_vec: Vec<u8>,
getrawtransaction_vec: Vec<u8>,
ancestry_headers_vec: Vec<Vec<u8>>,
) -> Self {
let gettxproof_bytes =
BoundedVec::<u8, ConstU32<UTXO_MAX_PROOF_BYTES>>::try_from(gettxproof_vec)
.expect("Should be valid UTXO gettxproof");
let getrawtransaction_bytes =
BoundedVec::<u8, ConstU32<UTXO_MAX_TX_BYTES>>::try_from(getrawtransaction_vec)
.expect("Should be valid UTXO proof");
let inner_bounded_headers = ancestry_headers_vec
.into_iter()
.map(|raw_header| {
BoundedVec::<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>::try_from(raw_header)
.expect("Should be valid header")
})
.collect::<Vec<BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>>>();
let ancestry_headers = BoundedVec::<
BoundedVec<u8, ConstU32<UTXO_MAX_HEADER_BYTES>>,
ConstU32<UTXO_MAX_HISTORY_DEPTH>,
>::try_from(inner_bounded_headers)
.expect("Should be valud UTXO headers");
Self {
o_index,
w_index,
expected_amount,
gettxproof_bytes,
getrawtransaction_bytes,
ancestry_headers,
payload,
}
}
}

View File

@ -1,23 +0,0 @@
use frame_support::weights::Weight;
pub trait WeightInfo {
fn extend_warp() -> Weight;
fn weave_weft() -> Weight;
fn pull_evm_thread() -> Weight;
fn pull_utxo_thread() -> Weight;
}
impl WeightInfo for () {
fn extend_warp() -> Weight {
Default::default()
}
fn weave_weft() -> Weight {
Default::default()
}
fn pull_evm_thread() -> Weight {
Default::default()
}
fn pull_utxo_thread() -> Weight {
Default::default()
}
}

Some files were not shown because too many files have changed in this diff Show More