|
| 1 | +//! Implementation of the faucet functionality |
| 2 | +//! |
| 3 | +//! This module contains the core logic for the faucet pallet, including: |
| 4 | +//! - The execution function that handles faucet requests |
| 5 | +//! - Hash calculation and verification functions for proof-of-work |
| 6 | +//! - Block hash retrieval and validation functions |
| 7 | +
|
| 8 | +use crate::Vec; |
| 9 | +use crate::{AccountIdOf, BalanceOf}; |
| 10 | +use crate::{Error, Event}; |
| 11 | +use codec::Encode; |
| 12 | +use polkadot_sdk::frame_support::{traits::Currency, LOG_TARGET}; |
| 13 | +use polkadot_sdk::polkadot_sdk_frame::prelude::BlockNumberFor; |
| 14 | +use polkadot_sdk::sp_core::{keccak_256, H256}; |
| 15 | +use polkadot_sdk::sp_core::{sha2_256, U256}; |
| 16 | +use polkadot_sdk::sp_runtime::{DispatchError, MultiAddress}; |
| 17 | +use polkadot_sdk::{ |
| 18 | + frame_support::ensure, |
| 19 | + frame_system, |
| 20 | + sp_tracing::{info, trace}, |
| 21 | +}; |
| 22 | + |
| 23 | +/// Main execution function for the faucet pallet |
| 24 | +/// |
| 25 | +/// This function processes a faucet request after it has passed the unsigned validation. |
| 26 | +/// It performs the following steps: |
| 27 | +/// 1. Ensures the account exists (or creates it) |
| 28 | +/// 2. Validates that the block number is recent (within the last 3 blocks) |
| 29 | +/// 3. Verifies that the proof-of-work meets the difficulty requirement |
| 30 | +/// 4. Checks that the submitted work hash matches the expected seal hash |
| 31 | +/// 5. Deposits tokens to the account if all checks pass |
| 32 | +/// |
| 33 | +/// # Parameters |
| 34 | +/// * `key` - The account that will receive tokens |
| 35 | +/// * `block_number` - The block number used for the proof-of-work |
| 36 | +/// * `nonce` - The nonce value that makes the hash meet the difficulty |
| 37 | +/// * `work` - The hash result to verify |
| 38 | +/// |
| 39 | +/// # Returns |
| 40 | +/// * `Ok(())` if successful |
| 41 | +/// * `Err` with an error if any check fails |
| 42 | +pub fn execute<T: crate::Config>( |
| 43 | + key: AccountIdOf<T>, |
| 44 | + block_number: u64, |
| 45 | + nonce: u64, |
| 46 | + work: Vec<u8>, |
| 47 | +) -> crate::DispatchResult { |
| 48 | + // Ensure the account exists in the system |
| 49 | + if <frame_system::Pallet<T>>::account(&key) == Default::default() { |
| 50 | + <frame_system::Pallet<T>>::inc_providers(&key); |
| 51 | + } |
| 52 | + |
| 53 | + info!("do faucet with key: {key:?} and block number: {block_number} and nonce: {nonce}"); |
| 54 | + |
| 55 | + // Get the current block number for validation |
| 56 | + let current_block_number: u64 = frame_system::Pallet::<T>::block_number() |
| 57 | + .try_into() |
| 58 | + .map_err(|_| "block number exceeded u64")?; |
| 59 | + |
| 60 | + // Ensure the block number is not in the future |
| 61 | + ensure!( |
| 62 | + block_number <= current_block_number, |
| 63 | + Error::<T>::InvalidWorkBlock |
| 64 | + ); |
| 65 | + |
| 66 | + // Ensure the block is recent (less than 3 blocks old) |
| 67 | + ensure!( |
| 68 | + current_block_number.saturating_sub(block_number) < 3, |
| 69 | + Error::<T>::InvalidWorkBlock |
| 70 | + ); |
| 71 | + |
| 72 | + // Validate the proof-of-work difficulty |
| 73 | + let difficulty: U256 = U256::from(1_000_000); |
| 74 | + let work_hash: H256 = H256::from_slice(&work); |
| 75 | + ensure!( |
| 76 | + hash_meets_difficulty(&work_hash, difficulty), |
| 77 | + Error::<T>::InvalidDifficulty |
| 78 | + ); |
| 79 | + |
| 80 | + // Verify that the submitted work hash matches the expected seal hash |
| 81 | + let seal: H256 = create_seal_hash::<T>(block_number, nonce, &key)?; |
| 82 | + ensure!(seal == work_hash, Error::<T>::InvalidSeal); |
| 83 | + |
| 84 | + // Award tokens to the account (15 tokens with 18 decimals) |
| 85 | + let amount: u64 = 15_000_000_000_000_000_000; |
| 86 | + let amount: BalanceOf<T> = amount.try_into().map_err(|_| "Invalid amount")?; |
| 87 | + let _ = T::Currency::deposit_creating(&key, amount); |
| 88 | + |
| 89 | + // Log success and emit event |
| 90 | + info!("faucet done successfully with key: {key:?} and amount: {amount:?})"); |
| 91 | + crate::Pallet::<T>::deposit_event(Event::<T>::Faucet(key, amount)); |
| 92 | + |
| 93 | + Ok(()) |
| 94 | +} |
| 95 | + |
| 96 | +/// Creates a hash combining the block hash and account key |
| 97 | +/// |
| 98 | +/// This function combines a block hash with an account key and produces a new hash. |
| 99 | +/// It takes the 32-byte block hash and combines it with the first 32 bytes of the |
| 100 | +/// account ID to create a 64-byte array, then hashes it with keccak-256. |
| 101 | +/// |
| 102 | +/// # Parameters |
| 103 | +/// * `block_hash_bytes` - The 32-byte hash of a block |
| 104 | +/// * `key` - The account ID to combine with the block hash |
| 105 | +/// |
| 106 | +/// # Returns |
| 107 | +/// * `Ok(H256)` - The resulting hash if successful |
| 108 | +/// * `Err` - If the key is too small |
| 109 | +pub fn hash_block_and_key<T: crate::Config>( |
| 110 | + block_hash_bytes: &[u8; 32], |
| 111 | + key: &T::AccountId, |
| 112 | +) -> Result<H256, DispatchError> { |
| 113 | + let key_pubkey: MultiAddress<_, ()> = MultiAddress::Id(key.clone()); |
| 114 | + let binding = key_pubkey.encode(); |
| 115 | + |
| 116 | + // Skip the first byte of the encoded key (which is a type indicator) |
| 117 | + let key_bytes = binding.get(1..).ok_or("Key is smaller than 1 byte")?; |
| 118 | + |
| 119 | + let mut full_bytes = [0u8; 64]; |
| 120 | + let (first_half, second_half) = full_bytes.split_at_mut(32); |
| 121 | + |
| 122 | + first_half.copy_from_slice(block_hash_bytes); |
| 123 | + |
| 124 | + second_half.copy_from_slice(key_bytes.get(..32).ok_or("Key is smaller than 32 bytes")?); |
| 125 | + |
| 126 | + let keccak_256_seal_hash_vec: [u8; 32] = keccak_256(&full_bytes[..]); |
| 127 | + |
| 128 | + Ok(H256::from_slice(&keccak_256_seal_hash_vec)) |
| 129 | +} |
| 130 | + |
| 131 | +/// Creates the seal hash used for proof-of-work verification |
| 132 | +/// |
| 133 | +/// This function generates the hash that users need to match with their proof-of-work. |
| 134 | +/// The process is: |
| 135 | +/// 1. Get the hash of the specified block |
| 136 | +/// 2. Concatenate the block hash with the account key |
| 137 | +/// 3. Concatenate the nonce with the result from step 2 |
| 138 | +/// 4. Hash the combined data with SHA-256 |
| 139 | +/// 5. Hash the result with keccak-256 |
| 140 | +pub fn create_seal_hash<T: crate::Config>( |
| 141 | + block_number: u64, |
| 142 | + nonce: u64, |
| 143 | + hotkey: &T::AccountId, |
| 144 | +) -> Result<H256, DispatchError> { |
| 145 | + let nonce = nonce.to_le_bytes(); |
| 146 | + |
| 147 | + let block_hash_at_number: H256 = get_block_hash_from_u64::<T>(block_number)?; |
| 148 | + let block_hash_bytes: &[u8; 32] = block_hash_at_number.as_fixed_bytes(); |
| 149 | + |
| 150 | + let binding = hash_block_and_key::<T>(block_hash_bytes, hotkey)?; |
| 151 | + let block_and_hotkey_hash_bytes: &[u8; 32] = binding.as_fixed_bytes(); |
| 152 | + |
| 153 | + let mut full_bytes = [0u8; 40]; |
| 154 | + let (first_chunk, second_chunk) = full_bytes.split_at_mut(8); |
| 155 | + first_chunk.copy_from_slice(&nonce); |
| 156 | + second_chunk.copy_from_slice(block_and_hotkey_hash_bytes); |
| 157 | + |
| 158 | + let sha256_seal_hash_vec: [u8; 32] = sha2_256(&full_bytes[..]); |
| 159 | + let keccak_256_seal_hash_vec: [u8; 32] = keccak_256(&sha256_seal_hash_vec); |
| 160 | + let seal_hash: H256 = H256::from_slice(&keccak_256_seal_hash_vec); |
| 161 | + |
| 162 | + trace!( |
| 163 | + "hotkey:{hotkey:?} \nblock_number: {block_number:?}, \nnonce_u64: {nonce:?}, \nblock_hash: {block_hash_at_number:?}, \nfull_bytes: {full_bytes:?}, \nsha256_seal_hash_vec: {sha256_seal_hash_vec:?}, \nkeccak_256_seal_hash_vec: {keccak_256_seal_hash_vec:?}, \nseal_hash: {seal_hash:?}", |
| 164 | + ); |
| 165 | + |
| 166 | + Ok(seal_hash) |
| 167 | +} |
| 168 | + |
| 169 | +/// Retrieves the hash of a block by its number |
| 170 | +/// |
| 171 | +/// This function converts a u64 block number to the chain's BlockNumberFor type, |
| 172 | +/// retrieves the hash of that block from the system, and converts it to an H256 type. |
| 173 | +pub fn get_block_hash_from_u64<T: crate::Config>(block_number: u64) -> Result<H256, DispatchError> { |
| 174 | + let block_number: BlockNumberFor<T> = block_number.try_into().map_err(|_| { |
| 175 | + "Block number {block_number} is too large to be converted to BlockNumberFor<T>" |
| 176 | + })?; |
| 177 | + |
| 178 | + let block_hash_at_number = frame_system::Pallet::<T>::block_hash(block_number); |
| 179 | + |
| 180 | + let vec_hash: Vec<u8> = block_hash_at_number.as_ref().to_vec(); |
| 181 | + let real_hash: H256 = H256::from_slice(&vec_hash); |
| 182 | + |
| 183 | + trace!( |
| 184 | + target: LOG_TARGET, |
| 185 | + "block_number: vec_hash: {vec_hash:?}, real_hash: {real_hash:?}", |
| 186 | + ); |
| 187 | + |
| 188 | + Ok(real_hash) |
| 189 | +} |
| 190 | + |
| 191 | +/// Checks if a hash meets the required difficulty for proof-of-work |
| 192 | +/// |
| 193 | +/// This function verifies that a hash meets the difficulty criteria by converting the |
| 194 | +/// hash to a U256 value and checking if multiplying it by the difficulty overflows. |
| 195 | +/// If there's no overflow, the hash meets the required difficulty. |
| 196 | +pub fn hash_meets_difficulty(hash: &H256, difficulty: U256) -> bool { |
| 197 | + let bytes: &[u8] = hash.as_bytes(); |
| 198 | + let num_hash: U256 = U256::from(bytes); |
| 199 | + |
| 200 | + // Multiply the hash value by the difficulty |
| 201 | + // If it overflows, the hash doesn't meet the difficulty requirement |
| 202 | + let (value, overflowed) = num_hash.overflowing_mul(difficulty); |
| 203 | + |
| 204 | + trace!( |
| 205 | + target: LOG_TARGET, |
| 206 | + "Difficulty: hash: {hash:?}, hash_bytes: {bytes:?}, hash_as_num: {num_hash:?}, difficulty: {difficulty:?}, value: {value:?} overflowed: {overflowed:?}", |
| 207 | + ); |
| 208 | + |
| 209 | + !overflowed |
| 210 | +} |
0 commit comments