Skip to content

Commit 000fe75

Browse files
authored
feat: add faucet pallet (#96)
This PR adds the faucet pallet. Closes CHAIN-85
1 parent 759e79c commit 000fe75

File tree

16 files changed

+952
-2
lines changed

16 files changed

+952
-2
lines changed

Cargo.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pallet-governance = { path = "./pallets/governance", default-features = false }
2222
pallet-governance-api = { path = "./pallets/governance/api", default-features = false }
2323
pallet-emission0 = { path = "./pallets/emission0", default-features = false }
2424
pallet-emission0-api = { path = "./pallets/emission0/api", default-features = false }
25+
pallet-faucet = { path = "./pallets/faucet", default-features = false }
2526
pallet-torus0 = { path = "./pallets/torus0", default-features = false }
2627
pallet-torus0-api = { path = "./pallets/torus0/api", default-features = false }
2728
pallet-permission0 = { path = "./pallets/permission0", default-features = false }

justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
default: check test
22

3+
# Build
4+
5+
build-mainnet:
6+
cargo build --release --timings --package torus-runtime
7+
8+
build-testnet:
9+
cargo build --release --features testnet --timings --package torus-runtime
10+
311
# Development
412

513
check:

pallets/faucet/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "pallet-faucet"
3+
description = "Testnet faucet pallet for the Torus network. Allows users to get test tokens by performing proof-of-work."
4+
version = "0.1.0"
5+
license = "MIT-0"
6+
authors.workspace = true
7+
edition.workspace = true
8+
9+
[features]
10+
default = ["std"]
11+
std = ["codec/std", "polkadot-sdk/std", "scale-info/std"]
12+
runtime-benchmarks = ["polkadot-sdk/runtime-benchmarks"]
13+
try-runtime = ["polkadot-sdk/try-runtime"]
14+
testnet = []
15+
16+
[dependencies]
17+
codec = { workspace = true, features = ["derive"] }
18+
scale-info = { workspace = true, features = ["derive"] }
19+
polkadot-sdk = { workspace = true, features = [
20+
"experimental",
21+
"runtime",
22+
"pallet-balances",
23+
] }
24+
25+
pallet-torus0-api.workspace = true
26+
27+
[dev-dependencies]
28+
pallet-governance.workspace = true
29+
pallet-torus0.workspace = true
30+
pallet-emission0.workspace = true
31+
pallet-permission0.workspace = true
32+
pallet-governance-api.workspace = true

pallets/faucet/src/ext.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//! Type aliases for the faucet pallet
2+
//!
3+
//! This module contains type definitions used throughout the faucet pallet code.
4+
5+
use polkadot_sdk::frame_support::traits::Currency;
6+
7+
/// Type alias for the Balance type used in the pallet
8+
///
9+
/// This represents the token amount type in the system, derived from the Currency configuration.
10+
pub(super) type BalanceOf<T> = <<T as crate::Config>::Currency as Currency<
11+
<T as polkadot_sdk::frame_system::Config>::AccountId,
12+
>>::Balance;
13+
14+
/// Type alias for the AccountId type used in the pallet
15+
///
16+
/// This represents the account identifier type in the system.
17+
pub(super) type AccountIdOf<T> = <T as polkadot_sdk::frame_system::Config>::AccountId;

pallets/faucet/src/faucet.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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

Comments
 (0)