forked from ghostchain/ghost-node
474 lines
14 KiB
Rust
474 lines
14 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, 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 index: u32,
|
|
pub token_id: TokenId,
|
|
pub shares: 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<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>;
|
|
|
|
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> {
|
|
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, network_id);
|
|
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.shares);
|
|
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
|
|
Ok(())
|
|
})?;
|
|
|
|
GlobalShares::<T>::try_mutate(|state| -> DispatchResult {
|
|
let increased = state.try_increase_claimed(claim_package.shares);
|
|
ensure!(increased.is_some(), Error::<T>::SharesOverflowed);
|
|
Ok(())
|
|
})?;
|
|
|
|
member_share.increase_activated_shares(claim_package.shares);
|
|
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 {
|
|
shares: claim_package.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 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, _>(
|
|
preimage,
|
|
&claim_package.merkle_proof,
|
|
network_state.merkle_root,
|
|
claim_package.index,
|
|
);
|
|
|
|
ensure!(is_merkle_proof_valid, Error::<T>::InvalidMerkleProof);
|
|
Ok(())
|
|
}
|
|
}
|