Interacting with UTXO
[!Note] Resolve all TODO in
runtime/src/utxo.rs
to complete this step.
Now that we have established the basics of our UTXO module, let's add ways to interact with it.
Reading Materials
Dispatchable / Call / Extrinsic functions:
[!Note] Note that we'll implement UTXO pallet in
dev_mode
, it is not necessary to specifyweight
andcall_index
. However, feel free to do it if you want.
Implement for Pallet UTXO
Implement spend
extrinsic. Highly recommend that keep the function simple and move all logics to intrinsics named with prefix do_...
, execute_...
(or whatever prefix you want 👀).
#![allow(unused)] fn main() { pub fn spend(_origin: OriginFor<T>, transaction: Transaction) -> DispatchResult { // validate transaction let transaction_validity = Self::validate_transaction(&transaction)?; ensure!( transaction_validity.requires.is_empty(), Error::<T>::MissingInput ); // implement logic Self::do_spend(&transaction, transaction_validity.priority as Value)?; // emit event Self::deposit_event(Event::<T>::TransactionSuccess(transaction)); Ok(()) } }
Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash.
Just is for demo usage, feel free to add logic of using additional salt yourself 😉.
#![allow(unused)] fn main() { fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> { let mut trx = transaction.clone(); for input in trx.inputs.iter_mut() { input.sigscript = H512::zero(); } trx.encode() } }
Implement validate_transaction intrinsic with criteria:
- Inputs and outputs are not empty
- Each input is used exactly once
- All inputs match to existing, unspent and unlocked outputs
- Sum of input values does not overflow
- Each output is defined exactly once and has nonzero value
- Sum of output values does not overflow
- Total output value must not exceed total input value
#![allow(unused)] fn main() { /// Check transaction for validity, errors, & race conditions /// Called by both transaction pool and runtime execution pub fn validate_transaction( transaction: &Transaction, ) -> Result<ValidTransaction, &'static str> { // Check inputs and outputs are not empty ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput); ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput); // Check each input is used exactly once { let input_set: BTreeMap<_, ()> = transaction.inputs.iter().map(|input| (input, ())).collect(); ensure!( input_set.len() == transaction.inputs.len(), Error::<T>::DuplicatedInput ); } { let output_set: BTreeMap<_, ()> = transaction .outputs .iter() .map(|output| (output, ())) .collect(); ensure!( output_set.len() == transaction.outputs.len(), Error::<T>::DuplicatedOutput ); } let mut total_input: Value = 0; let mut total_output: Value = 0; let mut output_index: u64 = 0; let simple_transaction = Self::get_simple_transaction(transaction); // Variables sent to transaction pool let mut missing_utxos = Vec::new(); let mut new_utxos = Vec::new(); let mut reward = 0; for input in transaction.inputs.iter() { // Check all inputs match to existing, unspent and unlocked outputs if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) { // Check provided signatures are valid let is_valid_sig = sp_io::crypto::sr25519_verify( &Signature::from_raw(*input.sigscript.as_fixed_bytes()), &simple_transaction, &Public::from_h256(input_utxo.pubkey), ); ensure!(is_valid_sig, Error::<T>::InvalidSignature); // Check sum of input values does not overflow total_input = total_input .checked_add(input_utxo.value) .ok_or(Error::<T>::InputOverflow)?; } else { missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec()); } } // Check each output is defined exactly once and has nonzero value for output in transaction.outputs.iter() { ensure!(output.value > 0, Error::<T>::ZeroAmount); let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index)); output_index = output_index .checked_add(1) .ok_or(Error::<T>::MaximumTransactionDepth)?; // Check new outputs do not collide with existing ones ensure!( !UtxoStore::<T>::contains_key(hash), Error::<T>::DuplicatedOutput ); // Check sum of output values does not overflow total_output = total_output .checked_add(output.value) .ok_or(Error::<T>::OutputOverflow)?; new_utxos.push(hash.as_fixed_bytes().to_vec()); } // If no race condition, check the math if missing_utxos.is_empty() { // Check total output value must not exceed total input value ensure!(total_input >= total_output, Error::<T>::OutputOverInput); reward = total_input .checked_sub(total_output) .ok_or(Error::<T>::RewardOverflow)?; } // Returns transaction details Ok(ValidTransaction { requires: missing_utxos, provides: new_utxos, priority: reward as u64, longevity: TransactionLongevity::max_value(), propagate: true, }) } }
Implement do_spend
intrinsic for spend
extrinsic
#![allow(unused)] fn main() { /// Implement spend logic, update storage to reflect changes made by transaction /// Where each UTXO key is a hash of the entire transaction and its order in the `TransactionOutputs` vector fn do_spend(transaction: &Transaction, reward: Value) -> DispatchResult { // Calculate new reward total. The rest of `total_input - total_output` will be used for block reward. let new_total = TotalReward::<T>::get() .checked_add(reward) .ok_or(Error::<T>::RewardOverflow)?; TotalReward::<T>::put(new_total); // Removing spent UTXOs for input in &transaction.inputs { UtxoStore::<T>::remove(input.outpoint); } let mut index: u64 = 0; for output in &transaction.outputs { let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index)); // validated before, this is safe index = index .checked_add(1) .ok_or(Error::<T>::MaximumTransactionDepth) .unwrap(); Self::store_utxo(output, hash); // Optional, this event I used for log Self::deposit_event(Event::TransactionOutputProcessed(hash)); } Ok(()) } }
Implement store_utxo
for storage mutation
#![allow(unused)] fn main() { fn store_utxo(utxo: &TransactionOutput, hash: H256) { // TODO [3-spend-utxo] UtxoStore::<T>::insert(hash, utxo); // further update 😉 } }
Build the code
cargo build --release
Awesome work completing this step! 🚀 You're doing great. Now, let's advance to next step.
#![allow(unused)] fn main() { // We make sure this pallet uses `no_std` for compiling to Wasm. #![cfg_attr(not(feature = "std"), no_std)] use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; use sp_core::{ sr25519::{Public, Signature}, ByteArray, H256, H512, }; use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion}; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use super::{block_author::BlockAuthor, issuance::Issuance}; pub use pallet::*; /// [2-data-structure] pub type Value = u128; /// [2-data-structure] /// Single transaction to be dispatched #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct Transaction { /// UTXOs to be used as inputs for current transaction pub inputs: Vec<TransactionInput>, /// UTXOs to be created as a result of current transaction dispatch pub outputs: Vec<TransactionOutput>, } /// [2-data-structure] /// Single transaction input that refers to one UTXO #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct TransactionInput { /// Reference to an UTXO to be spent pub outpoint: H256, /// Proof that transaction owner is authorized to spend referred UTXO & /// that the entire transaction is untampered pub sigscript: H512, } /// [2-data-structure] /// Single transaction output to create upon transaction dispatch #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct TransactionOutput { /// Value associated with this output pub value: Value, /// Public key associated with this output. In order to spend this output /// owner must provide a proof by hashing the whole `Transaction` and /// signing it with a corresponding private key. pub pubkey: H256, } #[frame_support::pallet(dev_mode)] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use super::*; /// [2-data-structure] #[pallet::config] pub trait Config: frame_system::Config { /// Because this pallet emits events, it depends on the runtime's definition of an event. /// Read more: https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/reference_docs/frame_runtime_types/index.html type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// A source to determine the block author type BlockAuthor: BlockAuthor; /// A source to determine the issuance portion of the block reward type Issuance: Issuance<BlockNumberFor<Self>, Value>; } #[pallet::pallet] pub struct Pallet<T>(_); /// [2-data-structure] /// Total reward value to be redistributed among authorities. /// It is accumulated from transactions during block execution /// and then dispersed to validators on block finalization. #[pallet::storage] #[pallet::getter(fn total_reward)] pub type TotalReward<T: Config> = StorageValue<_, Value, ValueQuery>; /// [2-data-structure] /// All valid unspent transaction outputs are stored in this map. /// Initial set of UTXO is populated from the list stored in genesis. /// We use the identity hasher here because the cryptographic hashing is /// done explicitly. /// Mapping from `BlakeTwo256::hash_of(transaction, index)` to `TransactionOutput` #[pallet::storage] #[pallet::getter(fn utxo_store)] pub type UtxoStore<T: Config> = StorageMap< Hasher = Identity, Key = H256, Value = TransactionOutput, QueryKind = OptionQuery, >; /// [2-data-structure] /// Pallets use events to inform users when important changes are made. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { /// Dispatch transaction successful TransactionSuccess(Transaction), /// UTXO out processed TransactionOutputProcessed(H256), /// Reward distributed to `BlockAuthor` RewardDistributed(Value, H256), /// Faucet to `To` Faucet(Value, H256), /// No one get reward RewardWasted, } /// [2-data-structure] /// Errors inform users that something went wrong. #[pallet::error] pub enum Error<T> { /// Missing `Transaction` Input MissingInput, /// Reward overflow RewardOverflow, /// Maximum transaction depth MaximumTransactionDepth, /// Empty input EmptyInput, /// Empty output EmptyOutput, /// Each input must only be used once DuplicatedInput, /// Each output must be defined only once DuplicatedOutput, /// Input value is overflow InputOverflow, /// Output value is overflow OutputOverflow, /// Output spent must lte than Input spent OutputOverInput, /// Zero amount spent ZeroAmount, /// Invalid signature InvalidSignature, } /// [2-data-structure] /// Define extrinsics / dispatchable function #[pallet::call] impl<T: Config> Pallet<T> { pub fn spend(_origin: OriginFor<T>, transaction: Transaction) -> DispatchResult { // TODO remove this todo!() // TODO [3-spend-utxo] // validate transaction // let transaction_validity = Self::validate_transaction(&transaction)?; // ensure!( // transaction_validity.requires.is_empty(), // Error::<T>::MissingInput // ); // // implement logic // Self::do_spend(&transaction, transaction_validity.priority as Value)?; // // emit event // Self::deposit_event(Event::<T>::TransactionSuccess(transaction)); // Ok(()) } } /// [2-data-structure] /// Define intrinsics impl<T: Config> Pallet<T> { /// Implement spend logic, update storage to reflect changes made by transaction /// Where each UTXO key is a hash of the entire transaction and its order in the `TransactionOutputs` vector fn do_spend(transaction: &Transaction, reward: Value) -> DispatchResult { // TODO remove this todo!(); // TODO [3-spend-utxo] // Calculate new reward total. The rest of `total_input - total_output` will be used for block reward. // let new_total = TotalReward::<T>::get() // .checked_add(reward) // .ok_or(Error::<T>::RewardOverflow)?; // TotalReward::<T>::put(new_total); // // Removing spent UTXOs // for input in &transaction.inputs { // UtxoStore::<T>::remove(input.outpoint); // } // let mut index: u64 = 0; // for output in &transaction.outputs { // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index)); // // validated before, this is safe // index = index // .checked_add(1) // .ok_or(Error::<T>::MaximumTransactionDepth) // .unwrap(); // Self::store_utxo(output, hash); // Self::deposit_event(Event::TransactionOutputProcessed(hash)); // } // Ok(()) } /// Redistribute combined reward value to block Author fn disperse_reward(author: &Public) { todo!(); } /// Mutate storage, insert / update new UTXOs fn store_utxo(utxo: &TransactionOutput, hash: H256) { // TODO remove this todo!(); // // TODO [3-spend-utxo] // UtxoStore::<T>::insert(hash, utxo); // // further update 😉 } /// Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash. fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> { // TODO remove this todo!(); // // TODO [3-spend-utxo] // let mut trx = transaction.clone(); // for input in trx.inputs.iter_mut() { // input.sigscript = H512::zero(); // } // trx.encode() } /// Check transaction for validity, errors, & race conditions /// Called by both transaction pool and runtime execution pub fn validate_transaction( transaction: &Transaction, ) -> Result<ValidTransaction, &'static str> { // TODO remove this todo!(); // TODO [3-spend-utxo] // // Check inputs and outputs are not empty // ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput); // ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput); // // Check each input is used exactly once // { // let input_set: BTreeMap<_, ()> = // transaction.inputs.iter().map(|input| (input, ())).collect(); // ensure!( // input_set.len() == transaction.inputs.len(), // Error::<T>::DuplicatedInput // ); // } // { // let output_set: BTreeMap<_, ()> = transaction // .outputs // .iter() // .map(|output| (output, ())) // .collect(); // ensure!( // output_set.len() == transaction.outputs.len(), // Error::<T>::DuplicatedOutput // ); // } // let mut total_input: Value = 0; // let mut total_output: Value = 0; // let mut output_index: u64 = 0; // let simple_transaction = Self::get_simple_transaction(transaction); // // Variables sent to transaction pool // let mut missing_utxos = Vec::new(); // let mut new_utxos = Vec::new(); // let mut reward = 0; // for input in transaction.inputs.iter() { // // Check all inputs match to existing, unspent and unlocked outputs // if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) { // // Check provided signatures are valid // let is_valid_sig = sp_io::crypto::sr25519_verify( // &Signature::from_raw(*input.sigscript.as_fixed_bytes()), // &simple_transaction, // &Public::from_h256(input_utxo.pubkey), // ); // ensure!(is_valid_sig, Error::<T>::InvalidSignature); // // Check sum of input values does not overflow // total_input = total_input // .checked_add(input_utxo.value) // .ok_or(Error::<T>::InputOverflow)?; // } else { // missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec()); // } // } // // Check each output is defined exactly once and has nonzero value // for output in transaction.outputs.iter() { // ensure!(output.value > 0, Error::<T>::ZeroAmount); // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index)); // output_index = output_index // .checked_add(1) // .ok_or(Error::<T>::MaximumTransactionDepth)?; // // Check new outputs do not collide with existing ones // ensure!( // !UtxoStore::<T>::contains_key(hash), // Error::<T>::DuplicatedOutput // ); // // Check sum of output values does not overflow // total_output = total_output // .checked_add(output.value) // .ok_or(Error::<T>::OutputOverflow)?; // new_utxos.push(hash.as_fixed_bytes().to_vec()); // } // // If no race condition, check the math // if missing_utxos.is_empty() { // // Check total output value must not exceed total input value // ensure!(total_input >= total_output, Error::<T>::OutputOverInput); // reward = total_input // .checked_sub(total_output) // .ok_or(Error::<T>::RewardOverflow)?; // } // // Returns transaction details // Ok(ValidTransaction { // requires: missing_utxos, // provides: new_utxos, // priority: reward as u64, // longevity: TransactionLongevity::max_value(), // propagate: true, // }) } } } }
#![allow(unused)] fn main() { // We make sure this pallet uses `no_std` for compiling to Wasm. #![cfg_attr(not(feature = "std"), no_std)] use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; use sp_core::{ sr25519::{Public, Signature}, ByteArray, H256, H512, }; use sp_runtime::traits::{BlakeTwo256, Hash, SaturatedConversion}; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use super::{block_author::BlockAuthor, issuance::Issuance}; pub use pallet::*; /// [2-data-structure] pub type Value = u128; /// [2-data-structure] /// Single transaction to be dispatched #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct Transaction { /// UTXOs to be used as inputs for current transaction pub inputs: Vec<TransactionInput>, /// UTXOs to be created as a result of current transaction dispatch pub outputs: Vec<TransactionOutput>, } /// [2-data-structure] /// Single transaction input that refers to one UTXO #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct TransactionInput { /// Reference to an UTXO to be spent pub outpoint: H256, /// Proof that transaction owner is authorized to spend referred UTXO & /// that the entire transaction is untampered pub sigscript: H512, } /// [2-data-structure] /// Single transaction output to create upon transaction dispatch #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)] pub struct TransactionOutput { /// Value associated with this output pub value: Value, /// Public key associated with this output. In order to spend this output /// owner must provide a proof by hashing the whole `Transaction` and /// signing it with a corresponding private key. pub pubkey: H256, } #[frame_support::pallet(dev_mode)] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use super::*; /// [2-data-structure] #[pallet::config] pub trait Config: frame_system::Config { /// Because this pallet emits events, it depends on the runtime's definition of an event. /// Read more: https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/reference_docs/frame_runtime_types/index.html type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// A source to determine the block author type BlockAuthor: BlockAuthor; /// A source to determine the issuance portion of the block reward type Issuance: Issuance<BlockNumberFor<Self>, Value>; } #[pallet::pallet] pub struct Pallet<T>(_); /// [2-data-structure] /// Total reward value to be redistributed among authorities. /// It is accumulated from transactions during block execution /// and then dispersed to validators on block finalization. #[pallet::storage] #[pallet::getter(fn total_reward)] pub type TotalReward<T: Config> = StorageValue<_, Value, ValueQuery>; /// [2-data-structure] /// All valid unspent transaction outputs are stored in this map. /// Initial set of UTXO is populated from the list stored in genesis. /// We use the identity hasher here because the cryptographic hashing is /// done explicitly. /// Mapping from `BlakeTwo256::hash_of(transaction, index)` to `TransactionOutput` #[pallet::storage] #[pallet::getter(fn utxo_store)] pub type UtxoStore<T: Config> = StorageMap< Hasher = Identity, Key = H256, Value = TransactionOutput, QueryKind = OptionQuery, >; /// [2-data-structure] /// Pallets use events to inform users when important changes are made. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { /// Dispatch transaction successful TransactionSuccess(Transaction), /// UTXO out processed TransactionOutputProcessed(H256), /// Reward distributed to `BlockAuthor` RewardDistributed(Value, H256), /// Faucet to `To` Faucet(Value, H256), /// No one get reward RewardWasted, } /// [2-data-structure] /// Errors inform users that something went wrong. #[pallet::error] pub enum Error<T> { /// Missing `Transaction` Input MissingInput, /// Reward overflow RewardOverflow, /// Maximum transaction depth MaximumTransactionDepth, /// Empty input EmptyInput, /// Empty output EmptyOutput, /// Each input must only be used once DuplicatedInput, /// Each output must be defined only once DuplicatedOutput, /// Input value is overflow InputOverflow, /// Output value is overflow OutputOverflow, /// Output spent must lte than Input spent OutputOverInput, /// Zero amount spent ZeroAmount, /// Invalid signature InvalidSignature, } /// [2-data-structure] /// Define extrinsics / dispatchable function #[pallet::call] impl<T: Config> Pallet<T> { pub fn spend(_origin: OriginFor<T>, transaction: Transaction) -> DispatchResult { // [3-spend-utxo] // validate transaction let transaction_validity = Self::validate_transaction(&transaction)?; ensure!( transaction_validity.requires.is_empty(), Error::<T>::MissingInput ); // implement logic Self::do_spend(&transaction, transaction_validity.priority as Value)?; // emit event Self::deposit_event(Event::<T>::TransactionSuccess(transaction)); Ok(()) } } /// [2-data-structure] /// Define intrinsics impl<T: Config> Pallet<T> { /// Implement spend logic, update storage to reflect changes made by transaction /// Where each UTXO key is a hash of the entire transaction and its order in the `TransactionOutputs` vector fn do_spend(transaction: &Transaction, reward: Value) -> DispatchResult { // [3-spend-utxo] // Calculate new reward total. The rest of `total_input - total_output` will be used for block reward. let new_total = TotalReward::<T>::get() .checked_add(reward) .ok_or(Error::<T>::RewardOverflow)?; TotalReward::<T>::put(new_total); // Removing spent UTXOs for input in &transaction.inputs { UtxoStore::<T>::remove(input.outpoint); } let mut index: u64 = 0; for output in &transaction.outputs { let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index)); // validated before, this is safe index = index .checked_add(1) .ok_or(Error::<T>::MaximumTransactionDepth) .unwrap(); Self::store_utxo(output, hash); Self::deposit_event(Event::TransactionOutputProcessed(hash)); } Ok(()) } /// Redistribute combined reward value to block Author fn disperse_reward(author: &Public) { todo!(); } /// Mutate storage, insert / update new UTXOs fn store_utxo(utxo: &TransactionOutput, hash: H256) { // [3-spend-utxo] UtxoStore::<T>::insert(hash, utxo); // // further update 😉 } /// Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash. fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> { // [3-spend-utxo] let mut trx = transaction.clone(); for input in trx.inputs.iter_mut() { input.sigscript = H512::zero(); } trx.encode() } /// Check transaction for validity, errors, & race conditions /// Called by both transaction pool and runtime execution pub fn validate_transaction( transaction: &Transaction, ) -> Result<ValidTransaction, &'static str> { // [3-spend-utxo] // Check inputs and outputs are not empty ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput); ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput); // Check each input is used exactly once { let input_set: BTreeMap<_, ()> = transaction.inputs.iter().map(|input| (input, ())).collect(); ensure!( input_set.len() == transaction.inputs.len(), Error::<T>::DuplicatedInput ); } { let output_set: BTreeMap<_, ()> = transaction .outputs .iter() .map(|output| (output, ())) .collect(); ensure!( output_set.len() == transaction.outputs.len(), Error::<T>::DuplicatedOutput ); } let mut total_input: Value = 0; let mut total_output: Value = 0; let mut output_index: u64 = 0; let simple_transaction = Self::get_simple_transaction(transaction); // Variables sent to transaction pool let mut missing_utxos = Vec::new(); let mut new_utxos = Vec::new(); let mut reward = 0; for input in transaction.inputs.iter() { // Check all inputs match to existing, unspent and unlocked outputs if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) { // Check provided signatures are valid let is_valid_sig = sp_io::crypto::sr25519_verify( &Signature::from_raw(*input.sigscript.as_fixed_bytes()), &simple_transaction, &Public::from_h256(input_utxo.pubkey), ); ensure!(is_valid_sig, Error::<T>::InvalidSignature); // Check sum of input values does not overflow total_input = total_input .checked_add(input_utxo.value) .ok_or(Error::<T>::InputOverflow)?; } else { missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec()); } } // Check each output is defined exactly once and has nonzero value for output in transaction.outputs.iter() { ensure!(output.value > 0, Error::<T>::ZeroAmount); let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index)); output_index = output_index .checked_add(1) .ok_or(Error::<T>::MaximumTransactionDepth)?; // Check new outputs do not collide with existing ones ensure!( !UtxoStore::<T>::contains_key(hash), Error::<T>::DuplicatedOutput ); // Check sum of output values does not overflow total_output = total_output .checked_add(output.value) .ok_or(Error::<T>::OutputOverflow)?; new_utxos.push(hash.as_fixed_bytes().to_vec()); } // If no race condition, check the math if missing_utxos.is_empty() { // Check total output value must not exceed total input value ensure!(total_input >= total_output, Error::<T>::OutputOverInput); reward = total_input .checked_sub(total_output) .ok_or(Error::<T>::RewardOverflow)?; } // Returns transaction details Ok(ValidTransaction { requires: missing_utxos, provides: new_utxos, priority: reward as u64, longevity: TransactionLongevity::max_value(), propagate: true, }) } } } }
diff --git a/runtime/src/utxo.rs b/runtime/src/utxo.rs
index 69a9535..a36637d 100644
--- a/runtime/src/utxo.rs
+++ b/runtime/src/utxo.rs
@@ -16,10 +16,10 @@ use super::{block_author::BlockAuthor, issuance::Issuance};
pub use pallet::*;
-/// [2-data-structure]
+/// [2-data-structure]
pub type Value = u128;
-/// [2-data-structure]
+/// [2-data-structure]
/// Single transaction to be dispatched
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)]
@@ -30,7 +30,7 @@ pub struct Transaction {
pub outputs: Vec<TransactionOutput>,
}
-/// [2-data-structure]
+/// [2-data-structure]
/// Single transaction input that refers to one UTXO
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)]
@@ -43,7 +43,7 @@ pub struct TransactionInput {
pub sigscript: H512,
}
-/// [2-data-structure]
+/// [2-data-structure]
/// Single transaction output to create upon transaction dispatch
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug, TypeInfo)]
@@ -64,7 +64,7 @@ pub mod pallet {
use super::*;
- /// [2-data-structure]
+ /// [2-data-structure]
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
@@ -72,9 +72,6 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// A source to determine the block author
- /// Read more: `runtime/src/block_author.rs`
- /// Pallet loosely coupling
- /// https://polkadot-blockchain-academy.github.io/pba-book/frame/coupling/page.html#loosely-coupled-pallets
type BlockAuthor: BlockAuthor;
/// A source to determine the issuance portion of the block reward
@@ -85,7 +82,7 @@ pub mod pallet {
pub struct Pallet<T>(_);
- /// [2-data-structure]
+ /// [2-data-structure]
/// Total reward value to be redistributed among authorities.
/// It is accumulated from transactions during block execution
/// and then dispersed to validators on block finalization.
@@ -93,7 +90,7 @@ pub mod pallet {
#[pallet::getter(fn total_reward)]
pub type TotalReward<T: Config> = StorageValue<_, Value, ValueQuery>;
- /// [2-data-structure]
+ /// [2-data-structure]
/// All valid unspent transaction outputs are stored in this map.
/// Initial set of UTXO is populated from the list stored in genesis.
/// We use the identity hasher here because the cryptographic hashing is
@@ -109,7 +106,7 @@ pub mod pallet {
>;
- /// [2-data-structure]
+ /// [2-data-structure]
/// Pallets use events to inform users when important changes are made.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
@@ -127,7 +124,7 @@ pub mod pallet {
}
- /// [2-data-structure]
+ /// [2-data-structure]
/// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
@@ -158,23 +155,65 @@ pub mod pallet {
}
- /// [2-data-structure]
+ /// [2-data-structure]
/// Define extrinsics / dispatchable function
#[pallet::call]
impl<T: Config> Pallet<T> {
pub fn spend(_origin: OriginFor<T>, transaction: Transaction) -> DispatchResult {
- todo!();
- }
+ // TODO remove this
+ todo!()
+
+ // TODO [3-spend-utxo]
+ // validate transaction
+ // let transaction_validity = Self::validate_transaction(&transaction)?;
+ // ensure!(
+ // transaction_validity.requires.is_empty(),
+ // Error::<T>::MissingInput
+ // );
+ // // implement logic
+ // Self::do_spend(&transaction, transaction_validity.priority as Value)?;
+ // // emit event
+ // Self::deposit_event(Event::<T>::TransactionSuccess(transaction));
+
+ // Ok(())
+ }
}
- /// [2-data-structure]
+ /// [2-data-structure]
/// Define intrinsics
impl<T: Config> Pallet<T> {
/// Implement spend logic, update storage to reflect changes made by transaction
/// Where each UTXO key is a hash of the entire transaction and its order in the `TransactionOutputs` vector
fn do_spend(transaction: &Transaction, reward: Value) -> DispatchResult {
+ // TODO remove this
todo!();
+
+ // TODO [3-spend-utxo]
+ // Calculate new reward total. The rest of `total_input - total_output` will be used for block reward.
+ // let new_total = TotalReward::<T>::get()
+ // .checked_add(reward)
+ // .ok_or(Error::<T>::RewardOverflow)?;
+ // TotalReward::<T>::put(new_total);
+
+ // // Removing spent UTXOs
+ // for input in &transaction.inputs {
+ // UtxoStore::<T>::remove(input.outpoint);
+ // }
+
+ // let mut index: u64 = 0;
+ // for output in &transaction.outputs {
+ // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index));
+ // // validated before, this is safe
+ // index = index
+ // .checked_add(1)
+ // .ok_or(Error::<T>::MaximumTransactionDepth)
+ // .unwrap();
+ // Self::store_utxo(output, hash);
+ // Self::deposit_event(Event::TransactionOutputProcessed(hash));
+ // }
+
+ // Ok(())
}
/// Redistribute combined reward value to block Author
@@ -184,12 +223,27 @@ pub mod pallet {
/// Mutate storage, insert / update new UTXOs
fn store_utxo(utxo: &TransactionOutput, hash: H256) {
+ // TODO remove this
todo!();
+
+ // // TODO [3-spend-utxo]
+ // UtxoStore::<T>::insert(hash, utxo);
+
+ // // further update 😉
}
/// Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash.
fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> {
+ // TODO remove this
todo!();
+
+ // // TODO [3-spend-utxo]
+ // let mut trx = transaction.clone();
+ // for input in trx.inputs.iter_mut() {
+ // input.sigscript = H512::zero();
+ // }
+
+ // trx.encode()
}
/// Check transaction for validity, errors, & race conditions
@@ -197,7 +251,100 @@ pub mod pallet {
pub fn validate_transaction(
transaction: &Transaction,
) -> Result<ValidTransaction, &'static str> {
+ // TODO remove this
todo!();
+
+ // TODO [3-spend-utxo]
+ // // Check inputs and outputs are not empty
+ // ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput);
+ // ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput);
+
+ // // Check each input is used exactly once
+ // {
+ // let input_set: BTreeMap<_, ()> =
+ // transaction.inputs.iter().map(|input| (input, ())).collect();
+ // ensure!(
+ // input_set.len() == transaction.inputs.len(),
+ // Error::<T>::DuplicatedInput
+ // );
+ // }
+ // {
+ // let output_set: BTreeMap<_, ()> = transaction
+ // .outputs
+ // .iter()
+ // .map(|output| (output, ()))
+ // .collect();
+ // ensure!(
+ // output_set.len() == transaction.outputs.len(),
+ // Error::<T>::DuplicatedOutput
+ // );
+ // }
+
+ // let mut total_input: Value = 0;
+ // let mut total_output: Value = 0;
+ // let mut output_index: u64 = 0;
+ // let simple_transaction = Self::get_simple_transaction(transaction);
+
+ // // Variables sent to transaction pool
+ // let mut missing_utxos = Vec::new();
+ // let mut new_utxos = Vec::new();
+ // let mut reward = 0;
+
+ // for input in transaction.inputs.iter() {
+ // // Check all inputs match to existing, unspent and unlocked outputs
+ // if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) {
+ // // Check provided signatures are valid
+ // let is_valid_sig = sp_io::crypto::sr25519_verify(
+ // &Signature::from_raw(*input.sigscript.as_fixed_bytes()),
+ // &simple_transaction,
+ // &Public::from_h256(input_utxo.pubkey),
+ // );
+ // ensure!(is_valid_sig, Error::<T>::InvalidSignature);
+ // // Check sum of input values does not overflow
+ // total_input = total_input
+ // .checked_add(input_utxo.value)
+ // .ok_or(Error::<T>::InputOverflow)?;
+ // } else {
+ // missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec());
+ // }
+ // }
+
+ // // Check each output is defined exactly once and has nonzero value
+ // for output in transaction.outputs.iter() {
+ // ensure!(output.value > 0, Error::<T>::ZeroAmount);
+ // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index));
+ // output_index = output_index
+ // .checked_add(1)
+ // .ok_or(Error::<T>::MaximumTransactionDepth)?;
+ // // Check new outputs do not collide with existing ones
+ // ensure!(
+ // !UtxoStore::<T>::contains_key(hash),
+ // Error::<T>::DuplicatedOutput
+ // );
+ // // Check sum of output values does not overflow
+ // total_output = total_output
+ // .checked_add(output.value)
+ // .ok_or(Error::<T>::OutputOverflow)?;
+ // new_utxos.push(hash.as_fixed_bytes().to_vec());
+ // }
+
+ // // If no race condition, check the math
+ // if missing_utxos.is_empty() {
+ // // Check total output value must not exceed total input value
+ // ensure!(total_input >= total_output, Error::<T>::OutputOverInput);
+ // reward = total_input
+ // .checked_sub(total_output)
+ // .ok_or(Error::<T>::RewardOverflow)?;
+ // }
+
+ // // Returns transaction details
+ // Ok(ValidTransaction {
+ // requires: missing_utxos,
+ // provides: new_utxos,
+ // priority: reward as u64,
+ // longevity: TransactionLongevity::max_value(),
+ // propagate: true,
+ // })
}
}
}
\ No newline at end of file
diff --git a/runtime/src/utxo.rs b/runtime/src/utxo.rs
index a36637d..00433df 100644
--- a/runtime/src/utxo.rs
+++ b/runtime/src/utxo.rs
@@ -160,23 +160,20 @@ pub mod pallet {
#[pallet::call]
impl<T: Config> Pallet<T> {
pub fn spend(_origin: OriginFor<T>, transaction: Transaction) -> DispatchResult {
- // TODO remove this
- todo!()
-
- // TODO [3-spend-utxo]
+ // [3-spend-utxo]
// validate transaction
- // let transaction_validity = Self::validate_transaction(&transaction)?;
- // ensure!(
- // transaction_validity.requires.is_empty(),
- // Error::<T>::MissingInput
- // );
-
- // // implement logic
- // Self::do_spend(&transaction, transaction_validity.priority as Value)?;
- // // emit event
- // Self::deposit_event(Event::<T>::TransactionSuccess(transaction));
-
- // Ok(())
+ let transaction_validity = Self::validate_transaction(&transaction)?;
+ ensure!(
+ transaction_validity.requires.is_empty(),
+ Error::<T>::MissingInput
+ );
+
+ // implement logic
+ Self::do_spend(&transaction, transaction_validity.priority as Value)?;
+ // emit event
+ Self::deposit_event(Event::<T>::TransactionSuccess(transaction));
+
+ Ok(())
}
}
@@ -186,34 +183,31 @@ pub mod pallet {
/// Implement spend logic, update storage to reflect changes made by transaction
/// Where each UTXO key is a hash of the entire transaction and its order in the `TransactionOutputs` vector
fn do_spend(transaction: &Transaction, reward: Value) -> DispatchResult {
- // TODO remove this
- todo!();
-
- // TODO [3-spend-utxo]
+ // [3-spend-utxo]
// Calculate new reward total. The rest of `total_input - total_output` will be used for block reward.
- // let new_total = TotalReward::<T>::get()
- // .checked_add(reward)
- // .ok_or(Error::<T>::RewardOverflow)?;
- // TotalReward::<T>::put(new_total);
-
- // // Removing spent UTXOs
- // for input in &transaction.inputs {
- // UtxoStore::<T>::remove(input.outpoint);
- // }
-
- // let mut index: u64 = 0;
- // for output in &transaction.outputs {
- // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index));
- // // validated before, this is safe
- // index = index
- // .checked_add(1)
- // .ok_or(Error::<T>::MaximumTransactionDepth)
- // .unwrap();
- // Self::store_utxo(output, hash);
- // Self::deposit_event(Event::TransactionOutputProcessed(hash));
- // }
-
- // Ok(())
+ let new_total = TotalReward::<T>::get()
+ .checked_add(reward)
+ .ok_or(Error::<T>::RewardOverflow)?;
+ TotalReward::<T>::put(new_total);
+
+ // Removing spent UTXOs
+ for input in &transaction.inputs {
+ UtxoStore::<T>::remove(input.outpoint);
+ }
+
+ let mut index: u64 = 0;
+ for output in &transaction.outputs {
+ let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index));
+ // validated before, this is safe
+ index = index
+ .checked_add(1)
+ .ok_or(Error::<T>::MaximumTransactionDepth)
+ .unwrap();
+ Self::store_utxo(output, hash);
+ Self::deposit_event(Event::TransactionOutputProcessed(hash));
+ }
+
+ Ok(())
}
/// Redistribute combined reward value to block Author
@@ -223,27 +217,21 @@ pub mod pallet {
/// Mutate storage, insert / update new UTXOs
fn store_utxo(utxo: &TransactionOutput, hash: H256) {
- // TODO remove this
- todo!();
-
- // // TODO [3-spend-utxo]
- // UtxoStore::<T>::insert(hash, utxo);
+ // [3-spend-utxo]
+ UtxoStore::<T>::insert(hash, utxo);
// // further update 😉
}
/// Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash.
fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> {
- // TODO remove this
- todo!();
-
- // // TODO [3-spend-utxo]
- // let mut trx = transaction.clone();
- // for input in trx.inputs.iter_mut() {
- // input.sigscript = H512::zero();
- // }
+ // [3-spend-utxo]
+ let mut trx = transaction.clone();
+ for input in trx.inputs.iter_mut() {
+ input.sigscript = H512::zero();
+ }
- // trx.encode()
+ trx.encode()
}
/// Check transaction for validity, errors, & race conditions
@@ -251,100 +239,97 @@ pub mod pallet {
pub fn validate_transaction(
transaction: &Transaction,
) -> Result<ValidTransaction, &'static str> {
- // TODO remove this
- todo!();
-
- // TODO [3-spend-utxo]
- // // Check inputs and outputs are not empty
- // ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput);
- // ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput);
-
- // // Check each input is used exactly once
- // {
- // let input_set: BTreeMap<_, ()> =
- // transaction.inputs.iter().map(|input| (input, ())).collect();
- // ensure!(
- // input_set.len() == transaction.inputs.len(),
- // Error::<T>::DuplicatedInput
- // );
- // }
- // {
- // let output_set: BTreeMap<_, ()> = transaction
- // .outputs
- // .iter()
- // .map(|output| (output, ()))
- // .collect();
- // ensure!(
- // output_set.len() == transaction.outputs.len(),
- // Error::<T>::DuplicatedOutput
- // );
- // }
-
- // let mut total_input: Value = 0;
- // let mut total_output: Value = 0;
- // let mut output_index: u64 = 0;
- // let simple_transaction = Self::get_simple_transaction(transaction);
-
- // // Variables sent to transaction pool
- // let mut missing_utxos = Vec::new();
- // let mut new_utxos = Vec::new();
- // let mut reward = 0;
-
- // for input in transaction.inputs.iter() {
- // // Check all inputs match to existing, unspent and unlocked outputs
- // if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) {
- // // Check provided signatures are valid
- // let is_valid_sig = sp_io::crypto::sr25519_verify(
- // &Signature::from_raw(*input.sigscript.as_fixed_bytes()),
- // &simple_transaction,
- // &Public::from_h256(input_utxo.pubkey),
- // );
- // ensure!(is_valid_sig, Error::<T>::InvalidSignature);
- // // Check sum of input values does not overflow
- // total_input = total_input
- // .checked_add(input_utxo.value)
- // .ok_or(Error::<T>::InputOverflow)?;
- // } else {
- // missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec());
- // }
- // }
-
- // // Check each output is defined exactly once and has nonzero value
- // for output in transaction.outputs.iter() {
- // ensure!(output.value > 0, Error::<T>::ZeroAmount);
- // let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index));
- // output_index = output_index
- // .checked_add(1)
- // .ok_or(Error::<T>::MaximumTransactionDepth)?;
- // // Check new outputs do not collide with existing ones
- // ensure!(
- // !UtxoStore::<T>::contains_key(hash),
- // Error::<T>::DuplicatedOutput
- // );
- // // Check sum of output values does not overflow
- // total_output = total_output
- // .checked_add(output.value)
- // .ok_or(Error::<T>::OutputOverflow)?;
- // new_utxos.push(hash.as_fixed_bytes().to_vec());
- // }
-
- // // If no race condition, check the math
- // if missing_utxos.is_empty() {
- // // Check total output value must not exceed total input value
- // ensure!(total_input >= total_output, Error::<T>::OutputOverInput);
- // reward = total_input
- // .checked_sub(total_output)
- // .ok_or(Error::<T>::RewardOverflow)?;
- // }
-
- // // Returns transaction details
- // Ok(ValidTransaction {
- // requires: missing_utxos,
- // provides: new_utxos,
- // priority: reward as u64,
- // longevity: TransactionLongevity::max_value(),
- // propagate: true,
- // })
+ // [3-spend-utxo]
+ // Check inputs and outputs are not empty
+ ensure!(!transaction.inputs.is_empty(), Error::<T>::EmptyInput);
+ ensure!(!transaction.outputs.is_empty(), Error::<T>::EmptyOutput);
+
+ // Check each input is used exactly once
+ {
+ let input_set: BTreeMap<_, ()> =
+ transaction.inputs.iter().map(|input| (input, ())).collect();
+ ensure!(
+ input_set.len() == transaction.inputs.len(),
+ Error::<T>::DuplicatedInput
+ );
+ }
+ {
+ let output_set: BTreeMap<_, ()> = transaction
+ .outputs
+ .iter()
+ .map(|output| (output, ()))
+ .collect();
+ ensure!(
+ output_set.len() == transaction.outputs.len(),
+ Error::<T>::DuplicatedOutput
+ );
+ }
+
+ let mut total_input: Value = 0;
+ let mut total_output: Value = 0;
+ let mut output_index: u64 = 0;
+ let simple_transaction = Self::get_simple_transaction(transaction);
+
+ // Variables sent to transaction pool
+ let mut missing_utxos = Vec::new();
+ let mut new_utxos = Vec::new();
+ let mut reward = 0;
+
+ for input in transaction.inputs.iter() {
+ // Check all inputs match to existing, unspent and unlocked outputs
+ if let Some(input_utxo) = UtxoStore::<T>::get(&input.outpoint) {
+ // Check provided signatures are valid
+ let is_valid_sig = sp_io::crypto::sr25519_verify(
+ &Signature::from_raw(*input.sigscript.as_fixed_bytes()),
+ &simple_transaction,
+ &Public::from_h256(input_utxo.pubkey),
+ );
+ ensure!(is_valid_sig, Error::<T>::InvalidSignature);
+ // Check sum of input values does not overflow
+ total_input = total_input
+ .checked_add(input_utxo.value)
+ .ok_or(Error::<T>::InputOverflow)?;
+ } else {
+ missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec());
+ }
+ }
+
+ // Check each output is defined exactly once and has nonzero value
+ for output in transaction.outputs.iter() {
+ ensure!(output.value > 0, Error::<T>::ZeroAmount);
+ let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index));
+ output_index = output_index
+ .checked_add(1)
+ .ok_or(Error::<T>::MaximumTransactionDepth)?;
+ // Check new outputs do not collide with existing ones
+ ensure!(
+ !UtxoStore::<T>::contains_key(hash),
+ Error::<T>::DuplicatedOutput
+ );
+ // Check sum of output values does not overflow
+ total_output = total_output
+ .checked_add(output.value)
+ .ok_or(Error::<T>::OutputOverflow)?;
+ new_utxos.push(hash.as_fixed_bytes().to_vec());
+ }
+
+ // If no race condition, check the math
+ if missing_utxos.is_empty() {
+ // Check total output value must not exceed total input value
+ ensure!(total_input >= total_output, Error::<T>::OutputOverInput);
+ reward = total_input
+ .checked_sub(total_output)
+ .ok_or(Error::<T>::RewardOverflow)?;
+ }
+
+ // Returns transaction details
+ Ok(ValidTransaction {
+ requires: missing_utxos,
+ provides: new_utxos,
+ priority: reward as u64,
+ longevity: TransactionLongevity::max_value(),
+ propagate: true,
+ })
}
}
}
\ No newline at end of file