ghost-node/pallets/governance/src/lib.rs
Uncle Stretch aa336d9c55
make proof size depend only on runtime constant
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-09-25 14:20:35 +03:00

551 lines
17 KiB
Rust

#![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, Saturating, UniqueSaturatedInto,
};
use num_traits::PrimInt;
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;
type TokenId = u128;
type ClaimNonce = u8;
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 = self.locked_balance
.saturating_add(extra_balance);
}
}
#[derive(Encode, Decode, RuntimeDebug, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ClaimPackage<T: pallet::Config> {
pub index: u32,
pub token_id: TokenId,
pub shares: BalanceOf<T>,
pub merkle_proof: BoundedVec<EvmHash, T::MaxProofDepth>,
}
impl<T: pallet::Config> Clone for ClaimPackage<T> {
fn clone(&self) -> Self {
Self {
index: self.index,
token_id: self.token_id,
shares: self.shares,
merkle_proof: self.merkle_proof.clone(),
}
}
}
impl<T: pallet::Config> PartialEq for ClaimPackage<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index &&
self.token_id == other.token_id &&
self.shares == other.shares &&
self.merkle_proof == other.merkle_proof
}
}
impl<T: pallet::Config> ClaimPackage<T> {
pub fn get_preimage<NetworkId>(
&self,
evm_address: &EvmAddress,
network_id: NetworkId
) -> [u8; 128]
where
NetworkId: Default + Copy + Clone + UniqueSaturatedInto<u128>,
{
let mut token_id_bytes = [0u8; 32];
U256::from(self.token_id as u128).to_big_endian(&mut token_id_bytes);
let mut shares_bytes = [0u8; 32];
let shares_u128: u128 = self.shares.unique_saturated_into();
U256::from(shares_u128).to_big_endian(&mut shares_bytes);
let mut address_bytes = [0u8; 32];
address_bytes[12..32].copy_from_slice(evm_address.as_bytes());
let mut network_id_bytes = [0u8; 32];
let network_id_u128: u128 = network_id.unique_saturated_into();
U256::from(network_id_u128).to_big_endian(&mut network_id_bytes);
let mut preimage = [0u8; 128];
preimage[0..32].copy_from_slice(&token_id_bytes);
preimage[32..64].copy_from_slice(&shares_bytes);
preimage[64..96].copy_from_slice(&address_bytes);
preimage[96..128].copy_from_slice(&network_id_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>;
#[pallet::constant]
type MaxPartialClaims: Get<ClaimNonce>;
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SharesClaimed {
who: T::AccountId,
shares: BalanceOf<T>,
network_id: NetworkIdOf<T>,
}
}
#[pallet::error]
pub enum Error<T> {
PartialClaimsExhausted,
BoundedToOtherNetwork,
AuthorityNotGovernor,
ExceedsAllowedShares,
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,
ClaimNonce,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn claimed_shares)]
pub type ClaimedShares<T: Config> = StorageMap<
_,
Twox64Concat, EvmHash,
BalanceOf<T>,
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(
claim_package.merkle_proof.len() as u32,
))]
pub fn claim(
origin: OriginFor<T>,
network_id: NetworkIdOf<T>,
claim_package: ClaimPackage<T>,
evm_address: EvmAddress,
evm_signature: EvmSignature,
requested_shares: BalanceOf<T>,
) -> 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, network_id);
let preimage_hash = SubstrateKeccakHasher::hash(&preimage);
let max_claim_nonces = T::MaxPartialClaims::get();
let current_nonce = ClaimedHashes::<T>::get(&preimage_hash);
ensure!(current_nonce < max_claim_nonces, Error::<T>::PartialClaimsExhausted);
let already_claimed = ClaimedShares::<T>::get(&preimage_hash);
let claimed_shares = if current_nonce == max_claim_nonces.saturating_sub(1) {
claim_package.shares.saturating_sub(already_claimed)
} else {
let cumulative_shares = already_claimed.saturating_add(requested_shares);
ensure!(cumulative_shares <= claim_package.shares, Error::<T>::ExceedsAllowedShares);
requested_shares
};
Self::verify_signature(&who, &network_id, &preimage_hash, &evm_address, &evm_signature, current_nonce)?;
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(claimed_shares);
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
Ok(())
})?;
GlobalShares::<T>::try_mutate(|state| -> DispatchResult {
let increased = state.try_increase_claimed(claimed_shares);
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
Ok(())
})?;
member_share.increase_activated_shares(claimed_shares);
member_share.initiate_on_network(network_id);
MemberShares::<T>::insert(&who, member_share);
ClaimedHashes::<T>::insert(&preimage_hash, current_nonce.saturating_add(1));
ClaimedShares::<T>::insert(
&preimage_hash,
already_claimed.saturating_add(claimed_shares),
);
Self::deposit_event(Event::<T>::SharesClaimed {
shares: claimed_shares,
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 to_ascii_base10<N: PrimInt + UniqueSaturatedInto<u128>>(num: N) -> Vec<u8> {
let zero = N::zero();
if num == zero { return sp_std::vec![b'0']; }
let ten = N::from(10).unwrap_or_else(|| N::zero());
let num_u128 = num.unique_saturated_into();
let capacity = (num_u128.ilog10() + 1) as usize;
let mut result = Vec::with_capacity(capacity);
let mut n = num;
while n > zero {
result.push(b'0' + (n % ten).to_u8().unwrap_or(0));
n = n / ten;
}
result.reverse();
result
}
fn ethereum_signable_message(
receiver: &T::AccountId,
network_id: &NetworkIdOf<T>,
preimage_hash: &EvmHash,
nonce: ClaimNonce,
) -> 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 = Self::to_ascii_base10(network_id_u64);
let nonce_ascii = Self::to_ascii_base10(nonce);
let account_label = b"\naccount:0x";
let preimage_label = b"\npreimage:0x";
let network_label = b"\nnetwork:";
let nonce_label = b"\nnonce:";
let what_length = prefix.len()
.saturating_add(account_label.len()).saturating_add(receiver_ascii_hex.len())
.saturating_add(preimage_label.len()).saturating_add(preimage_ascii_hex.len())
.saturating_add(network_label.len()).saturating_add(network_ascii.len())
.saturating_add(nonce_label.len()).saturating_add(nonce_ascii.len());
let mut what = Vec::with_capacity(what_length);
what.extend_from_slice(prefix);
what.extend_from_slice(account_label);
what.extend_from_slice(&receiver_ascii_hex);
what.extend_from_slice(preimage_label);
what.extend_from_slice(&preimage_ascii_hex);
what.extend_from_slice(network_label);
what.extend_from_slice(&network_ascii);
what.extend_from_slice(nonce_label);
what.extend_from_slice(&nonce_ascii);
let length_ascii = Self::to_ascii_base10(what.len());
let message_prefix = b"\x19Ethereum Signed Message:\n";
let message_capacity = message_prefix.len() + length_ascii.len() + what.len();
let mut v = Vec::with_capacity(message_capacity);
v.extend_from_slice(message_prefix);
v.extend_from_slice(&length_ascii);
v.extend(what);
v
}
fn verify_signature(
who: &T::AccountId,
network_id: &NetworkIdOf<T>,
preimage_hash: &EvmHash,
evm_address: &EvmAddress,
evm_signature: &EvmSignature,
nonce: ClaimNonce,
) -> DispatchResult {
let message = Self::ethereum_signable_message(who, network_id, preimage_hash, nonce);
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<T>,
) -> DispatchResult {
let network_state = NetworkShares::<T>::get(network_id);
let is_merkle_proof_valid =
verify_tree_proof::<SubstrateKeccakHasher, _>(
preimage,
&claim_package.merkle_proof,
network_state.merkle_root,
claim_package.index,
);
ensure!(is_merkle_proof_valid, Error::<T>::InvalidMerkleProof);
Ok(())
}
}