Important
Please take a look at sc_consensus_pow before dive deep into this.
Now, let's deep dive into Substrate Proof-of-Work!
Firstly, you need to create hashing algorithm. In this repository, it use multiple hashing algorithms MD5, SHA3, Keccak, see multi-pow.
Components:
Threshold: This struct represents the difficulty level for each supported hashing algorithm (MD5, Sha3, Keccak).
pub struct Threshold {
pub md5: U256,
pub sha3: U256,
pub keccak: U256,
}SupportedHashes: An enum that defines the supported hashing algorithms.
pub enum SupportedHashes {
Md5,
Sha3,
Keccak,
}MultiHash: A struct that stores a hash value and the hashing algorithm used to compute it.
pub struct MultiHash {
pub algo: SupportedHashes,
pub value: H256,
}Seal: This struct represents a PoW solution, including the computed hash (MultiHash), difficulty level (Threshold), and nonce.
pub struct Seal {
pub work: MultiHash,
pub difficulty: Threshold,
pub nonce: U256,
}Compute: This struct represents an attempt to solve the PoW puzzle before the hash is computed. It includes the difficulty level, pre-hash, and nonce.
pub struct Compute {
pub difficulty: Threshold,
pub pre_hash: H256,
pub nonce: U256,
}MultiPow: This struct is represent for our hashing algorithm. It provides functionalities for retrieving difficulty levels and verifying PoW solutions.
pub struct MultiPow<C> {
client: Arc<C>,
fork_config: ForkingConfig,
}ForkingConfig: This enum defines different forking configurations for handling changes in supported hashing algorithms.
pub enum ForkingConfig {
///
Manual,
///
Automatic(ForkHeights, MaxiPosition),
}simple_hash_meets_difficulty: This function checks if a given hash meets the difficulty level for a specific algorithm.multi_hash_meets_difficulty: This function checks if aMultiHashmeets the difficulty level specified in aThreshold.
pub fn simple_hash_meets_difficulty(hash: &H256, difficulty: U256) -> bool {
let num_hash = U256::from_little_endian(&hash[..]);
let (_, overflowed) = num_hash.overflowing_mul(difficulty);
!overflowed
}
pub fn multi_hash_meets_difficulty(hash: &MultiHash, difficulty: Threshold) -> bool {
match hash.algo {
SupportedHashes::Md5 => simple_hash_meets_difficulty(&hash.value, difficulty.md5),
SupportedHashes::Sha3 => simple_hash_meets_difficulty(&hash.value, difficulty.sha3),
SupportedHashes::Keccak => simple_hash_meets_difficulty(&hash.value, difficulty.keccak),
}
}manual_fork_validationandauto_fork_validation: These functions implement the validation logic based on the forking configuration and parent block height.
/// Manual mode, the node operator manually specifies which hashing algorithms are valid through the mining client.
/// If you would like to do a fork, simply allow, un-allow some algorithms to check it.
fn manual_fork_validation(_parent_number: u32, algo: SupportedHashes) -> bool {
use SupportedHashes::*;
// To begin with, allow all algorithms.
// After the fork height this check is skipped so all the hashes become valid.
match algo {
Md5 => true,
Sha3 => true,
Keccak => true,
}
}
/// In automatic mode, the `ForkHeights` and `MaxiPosition` structs define the forking schedule
/// and the node's behavior during the contentious fork
/// (where the network splits into two chains supporting different hashing algorithms).
/// The validation logic considers the parent block height,
/// forking configuration parameters, and the hashing algorithm used in the PoW solution to determine its validity.
fn auto_fork_validation(
parent_number: u32,
algo: SupportedHashes,
fork_heights: ForkHeights,
maxi_position: MaxiPosition,
) -> bool {
use MaxiPosition::*;
use SupportedHashes::*;
if parent_number < fork_heights.add_sha3_keccak {
// To begin with we only allow md5 hashes for our pow.
// After the fork height this check is skipped so all the hashes become valid.
match algo {
Md5 => true,
Sha3 => false,
Keccak => false,
}
} else if parent_number < fork_heights.remove_md5 {
// After the first fork, all three algos become valid.
match algo {
Md5 => true,
Sha3 => true,
Keccak => true,
}
} else if parent_number < fork_heights.split_sha3_keccak {
// After the second fork, md5 is no longer valid.
match algo {
Md5 => false,
Sha3 => true,
Keccak => true,
}
} else {
// Finally we have the contentious fork.
// Our behavior here depends which maxi position we have taken.
#[allow(clippy::match_like_matches_macro)]
match (algo, maxi_position) {
(Sha3, Sha3Maxi) => true,
(Sha3, NoMaxi) => true,
(Keccak, KeccakMaxi) => true,
(Keccak, NoMaxi) => true,
_ => false,
}
}
}Regarding the sc_consensus_pow: "To use this engine, you can need to have a struct that implements PowAlgorithm. After that, pass an instance of the struct, along with other necessary client references to import_queue to setup the queue.".
Hence, MultiPow has to implements the PowAlgorithm trait for the block validation and so on...
impl<B: BlockT<Hash = H256>, C> PowAlgorithm<B> for MultiPow<C>
where
C: ProvideRuntimeApi<B>,
C::Api: DifficultyApi<B, Threshold>,
C: sc_client_api::HeaderBackend<B>,
{
type Difficulty = Threshold;
fn difficulty(&self, parent: B::Hash) -> Result<Self::Difficulty, Error<B>> {
let difficulty = self
.client
.runtime_api()
.difficulty(parent)
.map_err(|err| {
sc_consensus_pow::Error::Environment(format!(
"Fetching difficulty from runtime failed: {:?}",
err
))
})?;
Ok(difficulty)
}
fn verify(
&self,
parent_id: &BlockId<B>,
pre_hash: &H256,
pre_digest: Option<&[u8]>,
seal: &RawSeal,
difficulty: Self::Difficulty,
) -> Result<bool, Error<B>> {
// Try to construct a seal object by decoding the raw seal given
let seal = match Seal::decode(&mut &seal[..]) {
Ok(seal) => seal,
Err(_) => return Ok(false),
};
log::debug!("✅ Decode seal passed!");
let Some(_encoded_pre_digest) = pre_digest else {
return Ok(false);
};
log::debug!("✅ Checksum digest passed!");
// This is where we handle forks on the verification side.
// We will still need to handle it in the mining algorithm somewhere.
// Currently we make the miner configure what algo they mine manually with their cli.
let parent_number: u32 = match parent_id {
BlockId::Hash(h) => *self
.client
.header(*h)
.expect("Database should perform lookup successfully")
.expect("parent header should be present in the db")
.number(),
BlockId::Number(n) => *n,
}
.try_into()
.map_err(|_| ())
.expect("Block numbers can be converted to u32 (because they are u32)");
log::debug!("✅ Checksum parent block number passed!");
// Here we handle the forking logic according the the node operator's request.
let valid_algorithm = match self.fork_config {
ForkingConfig::Manual => manual_fork_validation(parent_number, seal.work.algo),
ForkingConfig::Automatic(fork_heights, maxi_position) => {
auto_fork_validation(parent_number, seal.work.algo, fork_heights, maxi_position)
}
};
if !valid_algorithm {
return Ok(false);
}
log::debug!("✅ Valid algorithm!");
// See whether the hash meets the difficulty requirement. If not, fail fast.
if !multi_hash_meets_difficulty(&seal.work, difficulty) {
return Ok(false);
}
log::debug!("✅ Checksum difficulty passed!");
// Make sure the provided work actually comes from the correct pre_hash
let compute = Compute {
difficulty,
pre_hash: *pre_hash,
nonce: seal.nonce,
};
if compute.compute(seal.work.algo) != seal {
return Ok(false);
}
log::debug!("✅ Re-compute passed!");
log::debug!("🛠️ All passed, append the block to the chain ...");
Ok(true)
}
}This pallet allows block authors to self-identify by providing an sr25519 public key.
By default, the block author is set by miner following CLI by Inherent Transaction. See more in node/src/service.rs:239
A difficulty adjustment algorithm (DAA) to keep the block time close to a particular goal Cribbed from Kulupu https://github.com/kulupu/kulupu/blob/master/runtime/src/difficulty.rs
It is possible to implement other DAAs such as that of BTC and BCH. This would be an interesting and worth-while experiment. The DAAs should be abstracted away with a trait. Some ideas: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3410460
A type that provides block issuance according to Bitcoin's rules. Initial issuance is 50 / block.
Issuance is cut in half every 210,000 blocks
Cribbed from https://github.com/Bitcoin-ABC/bitcoin-abc/blob/9c7b12e6f128a59423f4de3d6d4b5231ebe9aac2/src/validation.cpp#L1007
Note
Approximately, 600 seconds (or 10 minutes) for a block to be finalized in Bitcoin blockchain. Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years.
Head over to utxo for more 😉.
Each PoW hashing algorithm is 1 pallet. Because in pallet difficulty I've defined it as generic pallet, so now just basically implement it with generic types.
construct_runtime!(
pub struct Runtime {
System: frame_system,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
+ Md5DifficultyAdjustment: difficulty::<Instance1>,
+ Sha3DifficultyAdjustment: difficulty::<Instance2>,
+ KeccakDifficultyAdjustment: difficulty::<Instance3>,
+ BlockAuthor: block_author,
+ Utxo: utxo,
}
);What is
Instance1,Instance2,Instance3?
Head over to Polkadot SDK doc - Module frame_support::instances.
frame-support provides some instance placeholder to be used in frame_support::pallet attribute macro.
...
parameter_types! {
pub const TargetBlockTime: u128 = 5_000;
// Setting min difficulty to damp factor per recommendation
pub const DampFactor: u128 = 3;
pub const ClampFactor: u128 = 2;
pub const MaxDifficulty: u128 = u128::max_value();
}
// Helper function to get the current blocks PoW algo from the predigest
fn current_blocks_mining_algo() -> SupportedHashes {
System::digest()
.logs
.iter()
.find_map(|digest_item| match digest_item {
DigestItem::PreRuntime(POW_ENGINE_ID, pre_digest) => {
PreDigest::decode(&mut &pre_digest[..]).map(|d| d.1).ok()
}
_ => None,
})
.expect("There should be exactly one pow pre-digest item")
}
impl difficulty::Config<Instance1> for Runtime {
type TimeProvider = Timestamp;
type TargetBlockTime = TargetBlockTime;
type DampFactor = DampFactor;
type ClampFactor = ClampFactor;
type MaxDifficulty = MaxDifficulty;
type MinDifficulty = DampFactor;
fn relevant_to_this_instance() -> bool {
current_blocks_mining_algo() == SupportedHashes::Md5
}
}
impl difficulty::Config<Instance2> for Runtime {
type TimeProvider = Timestamp;
type TargetBlockTime = TargetBlockTime;
type DampFactor = DampFactor;
type ClampFactor = ClampFactor;
type MaxDifficulty = MaxDifficulty;
type MinDifficulty = DampFactor;
fn relevant_to_this_instance() -> bool {
current_blocks_mining_algo() == SupportedHashes::Sha3
}
}
impl difficulty::Config<Instance3> for Runtime {
type TimeProvider = Timestamp;
type TargetBlockTime = TargetBlockTime;
type DampFactor = DampFactor;
type ClampFactor = ClampFactor;
type MaxDifficulty = MaxDifficulty;
type MinDifficulty = DampFactor;
fn relevant_to_this_instance() -> bool {
current_blocks_mining_algo() == SupportedHashes::Keccak
}
}Because the logic disperse block reward I've implemented on pallet utxo already, so this is not necessary. I just keep this for further building.
Feel free to change it to whatever you want like disperse block reward of utxo here instead of using hook, ...
impl block_author::Config for Runtime {
// Each block mined issues 50 new tokens to the miner
fn on_author_set(author_account: Self::AccountId) {
- let issuance = 50 * TOKEN;
- let _ = Balances::deposit_creating(&author_account, issuance);
}
}impl utxo::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
// BlockAuthor pallet
type BlockAuthor = BlockAuthor;
// Bitcoin halving rule
type Issuance = issuance::BitcoinHalving;
}By default, when you use some Substrate template, you usually see that it use GRANDPA (GHOST-based Recursive ANcestor Deriving Prefix Agreement) for block finality.
However, in this, we are building PoW blockchain, so we have to use PoW block validation & finality. Head over to node/service.rs
pub fn new_partial(
...
) ... {
...
// A block importer for PoW.
let block_import = sc_consensus_pow::PowBlockImport::new(
client.clone(),
client.clone(),
MultiPow::new(client.clone(), fork_config),
0, // check inherents starting at block 0
select_chain.clone(),
move |_, ()| async move {
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
let author =
academy_pow_runtime::block_author::InherentDataProvider(Default::default());
Ok((timestamp, author))
},
);
// Import queue for PoW engine.
let import_queue = sc_consensus_pow::import_queue(
Box::new(block_import.clone()),
None,
MultiPow::new(client.clone(), fork_config),
&task_manager.spawn_essential_handle(),
config.prometheus_registry(),
)?;
...
}
```rust
pub fn new_full(
...
) ... {
...
// define Mining Worker
let (mining_worker, mining_worker_task) = sc_consensus_pow::start_mining_worker(
Box::new(pow_block_import),
client.clone(),
select_chain,
MultiPow::new(client, fork_config),
proposer,
sync_service.clone(),
sync_service,
Some(PreDigest::from((sr25519_public_key.into(), mining_algo)).encode()),
move |_, ()| async move {
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
// set default `author` following miner specified in CLI
let author = academy_pow_runtime::block_author::InherentDataProvider(
sr25519_public_key.encode(),
);
Ok((timestamp, author))
},
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
);
task_manager.spawn_essential_handle().spawn_blocking(
"pow-miner",
Some("pow-mining"),
mining_worker_task,
);
// Start Mining worker.
use multi_pow::{multi_hash_meets_difficulty, Compute};
use sp_core::U256;
let mut nonce: U256 = U256::from(0);
std::thread::spawn(move || loop {
let worker = mining_worker.clone();
let metadata = worker.metadata();
if let Some(metadata) = metadata {
let compute = Compute {
difficulty: metadata.difficulty,
pre_hash: metadata.pre_hash,
nonce,
};
let seal = compute.compute(mining_algo);
if multi_hash_meets_difficulty(&seal.work, seal.difficulty) {
nonce = U256::from(0);
let _ = futures::executor::block_on(worker.submit(seal.encode()));
} else {
nonce = nonce.saturating_add(U256::from(1));
if nonce == U256::MAX {
nonce = U256::from(0);
}
}
} else {
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
}And the rest of code in chain_spec.rs and cli.rs are to start PoW node.