Skip to content

Instantly share code, notes, and snippets.

@sgerodes
Created January 20, 2024 05:09
Show Gist options
  • Save sgerodes/697a57882213980e80f6a69f3c6128d1 to your computer and use it in GitHub Desktop.
Save sgerodes/697a57882213980e80f6a69f3c6128d1 to your computer and use it in GitHub Desktop.
Francisos basics
//! # FRAME-less Runtime
//!
//! Welcome to the `FRAME-less` exercise, the fourth edition.
//!
//! > This assignment is based on Joshy's experiment years ago to explore building a Substrate
//! > runtime using pure Rust. If you learn something new in this exercise, attribute it to his
//! > work.
//!
//! Parts of this assignment resembles the `mini_substrate` section of the pre-course material. It
//! is recommended to re-familiarize yourself with that if you have done it. Nonetheless, everything
//! here is self-contained. Don't worry if you have not done `mini_substrate`.
//!
//! ## Context
//!
//! As the name suggests, this is FRAME-less runtime. It is a substrate-compatible runtime, which
//! you can easily run with companion `node`, without using `frame`.
//!
//! To run the `node`, execute `cargo run -- --dev`, possibly with `--release`. `--dev` will ensure
//! that a new database is created each time, and your chain starts afresh.
//!
//! While you are welcome to explore the `node` folder, it is not part of this assignment, and you
//! can leave it as-is.
//!
//! This node operates on a test block-authoring/consensus scheme, where blocks are produced at
//! fixed intervals. See `--consensus` cli option if you want to speed the block production up or
//! down.
//!
//! ## Assignment
//!
//! ### Overview
//!
//! You will design a simple substrate runtime with the following properties in this assignment:
//!
//! - Only signed extrinsics are accepted, so you will learn signature verification.
//! - Basic calls for testing/learning, mainly represented in [`shared::RuntimeCall::System`].
//! - Basic currency system.
//! - Basic staking/reserving system.
//! - Nonce system, to prevent replay attacks and similar issues.
//! - Tipping, which is there to mimic transaction fee payment.
//!
//! > Given that we have no notion of inherents or unsigned extrinsics here, the words transaction
//! > and extrinsic means the same thing in this assignment.
//!
//! Read the rest of this file and [`shared`] carefully, as it is your main specification of what
//! you have to implement.
//!
//! ### Prelude: Knowledge Recap
//!
//! #### Block Authoring and Importing
//!
//! Recall that the block author does the following:
//!
//! ```no_compile
//! (possibly call into `validate_transaction` periodically)
//!
//! Core::initialize_block(raw_header)
//! loop {
//! BlockBuilder::apply_extrinsic(ext)
//! }
//! BlockBuilder::finalize_block() -> final_header
//! ```
//!
//! And the block importer only calls into:
//!
//! ```no_compile
//! Core::execute_block(block)
//! ```
//!
//! We need to make sure that these two code paths each record the **correct and equal** state and
//! extrinsic root in the header. More about this in step 0. In other words, these two code paths
//! must execute the exact same logic and produce the exact same side effects.
//!
//! #### Apply vs. Dispatch
//!
//! When an extrinsic passes enough mandatory checks to justify its existence in a block, we call
//! this extrinsic to be apply-able. In other words, the extrinsic will get executed while authoring
//! and importing phase.
//!
//! Checks that must happen in apply phase are those that are mandatory to make sure a blockchain is
//! sound and safe. These include:
//!
//! - Signature verification
//! - Payment of any fees and tips.
//! - Nonce check.
//!
//! > For each of the above, take a moment to think about what happens if we don't do them. How is
//! > that blockchain vulnerable?
//!
//! Failure to meet any of these requirements means that this transaction is not even worth being in
//! the block and it should be discarded in the authoring phase. This is what we mean by a "failed
//! apply", and it is represented `ApplyExtrinsicResult` being `Err(_)`.
//!
//! Any other node importing a block expects the author to never place a transaction that cannot be
//! applied in the block. See if you can find the part of the code that deals with this, in both
//! this assignment and FRAME.
//!
//! The outcome of *successful applying* can itself be a failure or success. This inner execution of
//! the extrinsic is called "dispatch".
//!
//! Contrary, a dispatch error means that the extrinsic *is worth keeping in the block*, but
//! whatever it wished to do may have failed. This is represented by `ApplyExtrinsicResult` being
//! `Ok(Err(_))`.
//!
//! Use this information to return the correct `ApplyExtrinsicResult` in `do_apply_extrinsic`.
//!
//! #### Transaction Pool Validation
//!
//! Recall that the transaction pool can asynchronously, and at arbitrary intervals, ask the runtime
//! to validate a given transactions. Two rule of thumbs about this:
//!
//! 1. The transaction pool validation must be cheap and static. As in, you MUST NOT dispatch
//! anything in the pool validation phase.
//! 2. The transaction pool validation must contain all the checks that make a transaction
//! apply-able.
//!
//! Recall that the state changes of the transaction pool API are always discarded.
//!
//! #### Extrinsic Format
//!
//! The extrinsic format in this assignment is defined in `shared.rs`:
#![doc = docify::embed!("src/shared.rs", Extrinsic)]
//!
//! When this extrinsic is unpacked, the runtime will first look at the signature, tip and nonce. If
//! they meet all relevant conditions, this transaction is apply-able, and should be dispatched.
//! This depends on the inner [`shared::RuntimeCall`]. This type represent the different modules in
//! your runtime.
//!
//! #### Storage
//!
//! You will need to alter the runtime state in this assignment. Use `sp_io::storage` apis for this.
//! You are welcome to create more ergonomic abstractions on top of this.
//!
//! ### Step 0 - Basics
//!
//! In this section you implement all the fundamental parts of your runtime. This step is slightly
//! longer than the rest. Make sure to spend enough time on it as it it is the foundation for the
//! rest of the assignment.
//!
//! - **Proper signature check**. You need to look into `UncheckedExtrinsic.signature`, ensure it is
//! `Some`, and only accept those that are properly signed. The signing payload should be the
//! entire `UncheckedExtrinsic.function` (ignore `Extra`, which is `()`). Since
//! `UncheckedExtrinsic` is the type that is used in all substrate-based chains, you can look at
//! the methods and traits implemented for this type for inspiration. For example, look into `impl
//! Checkable for UncheckedExtrinsic`.
//! - **Implement `SystemCall`**. Once you verify the signature, you can get a signer `AccountId`
//! out of the extrinsic. Use this to implement [`shared::SystemCall`] dispatchables, such as
//! `SetValue`.
//!
//! The above two steps should both be implemented as a part of `apply_extrinsic`.
//!
//! - **Root calculation**: Once you have successful transactions processed by your runtime you need
//! to make sure your state and extrinsic root are correct.
//! - After each successful `apply_extrinsic`, if the transaction passes all the checks to be
//! apply-able, note the encoded extrinsic in `EXTRINSICS_KEY`.
//! - In `finalize_block`, compute the extrinsics root from the above.
//! - Flush the `EXTRINSICS_KEY` at the beginning of the next block authoring's
//! `initialize_block`.
//!
//! The `execute_block` given to you is already complete. You can look into it and reverse-engineer
//! how to calculate roots, and what is expected of your in the block authoring part.
//!
//! In all of the above you mainly need to finish `do_apply_extrinsic` and `do_finalize_block`.
//!
//! Lastly, you need to update `validate_transaction` to make sure transactions with bad signature
//! are rejected.
//!
//! By the end of this section, you should be able to pass all the unit tests provided to you.
//!
//! Most importantly, the following test ensures that your block authoring and importing are equal
//! and correct.
#![doc = docify::embed!("src/lib.rs", import_and_author_equal)]
//!
//! Also, if you run your chain with two nodes, you will be able to test this property. Lastly, you
//! are advised to use a JSON-RPC client at this stage and try simple transactions like `SetValue`
//! and observe their effects. See the material in "Interacting with Substrate" lecture.
//!
//! #### Apply Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::BadProof`] if the extrinsic has an
//! invalid signature.
//!
//! #### Transaction Pool Validation Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::BadProof`] if the extrinsic has an
//! invalid signature.
//!
//! ### 1 - Currency
//!
//! Look into [`shared::CurrencyCall`] and implement the requirement as specified in the docs. Pay
//! close attention to the existential deposit section.
//!
//! ### 2 - Staking
//!
//! Look into [`shared::StakingCall`] and implement the requirement as specified in the docs.
//!
//! ### 3 - Tipping
//!
//! The ability to tip is baked into [`shared::RuntimeCallExt::tip`]. Up until this point, you were
//! expected to ignore this field.
//!
//! The tip is meant to represent transaction fees. But, to keep the assignment simple, they are
//! *optional*, therefore naming them "tip".
//!
//! If [`shared::RuntimeCallExt::tip`] is `Some(_)`, the sender's ability to pay this amount is now
//! a mandatory condition for the transaction to be apply-able. This means it must be checked in
//! both:
//!
//! * `apply_extrinsic`
//! * `validate_transaction`
//!
//! **Paying the tip must not cause an account's existence to change. Specifically, an account
//! cannot be destroyed due to the tip**. This is because tip payment happens prior to dispatch, and
//! the dispatch logic of the runtime assumes all accounts start at an "existing" state.
//!
//! > For example, an account that has 20 tokens cannot transfer 5 and tip 15, but it can transfer
//! > 15 and tip 5.
//!
//! All tips are transferred to [`shared::TREASURY`], as if [`shared::TREASURY`] was an account's
//! public key. That is, the account is stored under "BalancesMap", and uses the same
//! `AccountBalance`, but it can only ever have `free` balance.
//!
//! The only exception of account existence is about [`shared::TREASURY`]. If this account is
//! non-existent, it can receive tips that are smaller than `EXISTENTIAL_DEPOSIT`. If by the end of
//! the dispatch the [`shared::TREASURY`] still has less than `EXISTENTIAL_DEPOSIT`, this amount
//! should be burnt. Burning is the opposite of minting, and should update the total issuance as
//! well.
//!
//! #### Apply Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::Payment`] if the extrinsic cannot pay
//! for its declared tip.
//!
//! #### Transaction Pool Validation Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::Payment`] if the extrinsic cannot pay
//! for its declared tip.
//!
//! If successful, the `priority` of the transaction should be increased by the tip amount, when
//! when converted to u64. Saturating conversion should be used.
//!
//! ### Nonce
//!
//! The last field of [`shared::AccountBalance`] that has been thus far ignored is the nonce.
//!
//! You should implement a nonce system, using the `provides` and `requires` fields explained in the
//! tx-pool lecture. In short, when applying a transaction, it should `require` nonce `(sender,
//! n-1).encode()` and provide `(sender, n).encode()`. Validation follows a somewhat similar,
//! pattern. Note how we prefix the nonce with the account-ids to cluster nonces per accounts.
//!
//! All accounts are created with nonce 0. An account that is created and then destroyed and then
//! re-created is no exception. The first valid transaction after an account created must have nonce
//! 0, which if successful, will set the account nonce to 1. Next valid transaction has nonce 1 and
//! so on.
//!
//! ## Apply Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::Future`] or `Stale` if the transaction's
//! nonce is not correct.
//!
//! ## Transaction Pool Validation Errors
//!
//! [`sp_runtime::transaction_validity::InvalidTransaction::Future`] or `Stale` if the transaction's
//! nonce is not correct.
//!
//! If valid, the correct `provides` and `requires` should be set.
//!
//! Look into [`Runtime::validate_nonce`] to understand how nonce system needs to be implemented for
//! validating transaction. Additionally, nonce are verified and incremented as well as part of
//! [`Runtime::apply_predispatch`].
//!
//! > Note that unlike signature check, account existence and tipping, the behavior of
//! > `apply_extrinsic` and `validate_transaction` is noticeably different with respect to nonce.
//!
//! ## Checklist (TL;DR)
//!
//! Here's a quick summary of the major action items, in the same order as specified above.
//!
//! - [ ] (0.1) Implement signature verification in [`Runtime::do_apply_extrinsic`]. Refer
//! [`Runtime::verify_signed`].
//! - [ ] (0.2) Implement your first set of dispatchables in [`shared::SystemCall`] while applying
//! extrinsics. Refer [`Runtime::apply_dispatch`] and invoke it in
//! [`Runtime::do_apply_extrinsic`].
//! - [ ] (0.3) Once an extrinsic passes all the `predispatch checks` while apply,
//! [`Runtime::note_extrinsic`] in the block.
//! - [ ] (0.4) When all extrinsics in a block has been applied, compute extrinsic root and state
//! root in the `finalize_block`, and set it in the header. At this point all the provided unit
//! tests should pass. See [`Runtime::update_header`].
//! - [ ] (1) Implement the [currency module](`shared::CurrencyCall`) in your runtime. Make sure: -
//! [ ] Account state is always in valid before and after dispatch (`Created` or `Destroyed`).
//! - [ ] Total issuance is maintained correctly at all times.
//! - [ ] (2) Build a [staking system](`shared::StakingCall`) on top of the currency module.
//! - [ ] (3) Ability to add an optional tip while submitting a transaction. Refer
//! [`Runtime::validate_tip`] and [`Runtime::apply_predispatch`].
//! - [ ] (4) Prevent replay attacks by adding a nonce system for user transactions. Refer
//! [`Runtime::validate_nonce`] and [`Runtime::apply_predispatch`].
//!
//! ## Grading
//!
//! This assignment is primarily graded through automatic tests, not by looking at the internals of
//! your runtime. We will manually look into each student's code and provide feedback as well, but
//! this will not contribute to your grade. This means you should be very careful about adhering to
//! the rules and specifications.
//!
//! Automatic Wasm grading means:
//!
//! * we do not care about the internals of your runtime, other than the standard set of runtime
//! apis.
//! * we do not care if you derive some additional trait for some type anywhere.
//! * but we do care about your storage layout being exactly as described in [`shared`].
//! * we do care about the extrinsic format being exactly as described in [`shared`].
//! * our tests are fairly similar to `import_and_author_equal`. We construct a list of extrinsics,
//! some of which are successful and some are not. Those that were at least apply-able are put
//! into an authored block. Then, we re-import this block. Finally, we assert that authoring and
//! importing the block had the same side effects and roots.
//!
//! While we can't force you not to change [`shared`] module, we use an exact copy of this file to
//! craft extrinsics/blocks to interact with your runtime while grading, and we expect to find the
//! types mentioned in there (eg. [`shared::AccountBalance`]) to be what we decode from your
//! storage.
//!
//! That being said, you can use types that are equivalent to their encoding to the ones mentioned
//! in [`shared`].
//!
//! ### Deadline
//!
//! The deadline for the assignment is Saturday 10pm.
//!
//! ### PBA-4 Hong Kong ONLY
//!
//! For this PBA round, you can ignore all details about the staking module.
//!
//! ### Rubric
//!
//! TBA
//!
//! ## Hints
//!
//! ### Logging
//!
//! Logging can be enabled by setting the `RUST_LOG` environment variable, as such:
//!
//! ```no_compile
//! RUST_LOG=frameless=debug cargo run
//! ```
//!
//! Or equally:
//!
//! ```no_compile
//! cargo run -- --dev -l frameless=debug
//! ```
//!
//! ### Running Two Nodes
//!
//! In order to run two nodes, execute the following commands in two different terminals.
//!
//! ```no_compile
//! cargo run -- --dev --alice -l frameless=debug
//! cargo run -- --dev --bob -l frameless=debug --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/<node-id-of-alice>
//! ```
//!
//! If you let the former `--alice` node progress for a bit, you will see that `--bob` will start
//! syncing from alice.
//!
//! ### Extra: `SignedExtensions`
//!
//! What we have implemented tip and nonce in this assignment as added fields to our
//! [`shared::RuntimeCallExt`], they should have ideally been implemented as a "signed extension".
//! In a separate branch, explore this, and ask for our feedback. If make progress on this front, DO
//! NOT submit it for grading, as our grading will work with the simpler `RuntimeCallExt` model.
//!
//! This is entirely optional.
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
const LOG_TARGET: &'static str = "frameless";
pub mod shared;
use crate::shared::{AccountId, EXTRINSICS_KEY, HEADER_KEY, RuntimeCall};
use log::info;
use opaque::Header;
use parity_scale_codec::{Decode, Encode};
use shared::Block;
use sp_api::impl_runtime_apis;
use sp_core::{hexdisplay::HexDisplay, OpaqueMetadata, H256, ecdsa::Signature};
use sp_runtime::{
create_runtime_str,
generic::{self},
traits::{BlakeTwo256, Block as BlockT, Hash, Verify},
transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction},
ApplyExtrinsicResult, DispatchError,
};
use sp_std::prelude::*;
#[cfg(feature = "std")]
use sp_version::NativeVersion;
use sp_version::RuntimeVersion;
/// Opaque types. This is what the lectures referred to as `ClientBlock`. Notice how
/// `OpaqueExtrinsic` is merely a `Vec<u8>`.
pub mod opaque {
use super::*;
type OpaqueExtrinsic = sp_runtime::OpaqueExtrinsic;
/// Opaque block header type.
pub type Header = generic::Header<shared::BlockNumber, BlakeTwo256>;
/// Opaque block type.
pub type Block = generic::Block<Header, OpaqueExtrinsic>;
}
/// This runtime version.
#[sp_version::runtime_version]
pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("frameless-runtime"),
impl_name: create_runtime_str!("frameless-runtime"),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
state_version: 1,
};
/// The version information used to identify this runtime when compiled natively. This is almost
/// deprecated. Ignore.
#[cfg(feature = "std")]
pub fn native_version() -> NativeVersion {
NativeVersion { runtime_version: VERSION, can_author_with: Default::default() }
}
/// The main struct in this module. In frame this comes from `construct_runtime!` macro.
#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone)]
pub struct Runtime;
// This impl block contains just some utilities that we have provided for you. You are free to use
// or ignore them.
#[allow(unused)]
impl Runtime {
/// Print the entire state as a `trace` log.
fn print_state() {
let mut key = vec![];
while let Some(next) = sp_io::storage::next_key(&key) {
let val = sp_io::storage::get(&next).unwrap().to_vec();
log::trace!(
target: LOG_TARGET,
"{} <=> {}",
HexDisplay::from(&next),
HexDisplay::from(&val)
);
key = next;
}
}
/// Get the state value at `key`, expected to decode into `T`.
fn get_state<T: Decode>(key: &[u8]) -> Option<T> {
sp_io::storage::get(key).and_then(|d| T::decode(&mut &*d).ok())
}
/// Mutate the value under `key`, expected to be of type `T` using `update`.
///
/// `update` contains `Some(T)` if a value exists under `T`, `None` otherwise
fn mutate_state<T: Decode + Encode + Default>(key: &[u8], update: impl FnOnce(&mut T)) {
let mut value = Self::get_state(key).unwrap_or_default();
update(&mut value);
sp_io::storage::set(key, &value.encode());
}
/// if you want some initial state in your own local test (when you actually run the node with
/// `cargo run`), then add them here. We don't ever call into this API.
pub fn do_build_config() -> sp_genesis_builder::Result {
Ok(())
}
}
// This impl block contains the core runtime api implementations. It contains good starting points
// denoted as a `FIXME`.
impl Runtime {
pub fn do_initialize_block(header: &<Block as BlockT>::Header) {
sp_io::storage::set(&HEADER_KEY, &header.encode());
sp_io::storage::clear(&EXTRINSICS_KEY);
}
pub fn do_finalize_block() -> <Block as BlockT>::Header {
// fetch the header that was given to us at the beginning of the block.
let mut header = Self::get_state::<<Block as BlockT>::Header>(HEADER_KEY)
.expect("We initialized with header, it never got mutated, qed");
// and make sure to _remove_ it.
sp_io::storage::clear(&HEADER_KEY);
// This print is only for logging and debugging. Remove it.
Runtime::print_state();
Self::update_header(header)
// FIXME: update the header to contain to correct state and extrinsic root.
// todo!();
}
/// Apply a single extrinsic.
///
/// In our template, we call into this from both block authoring, and block import.
pub fn do_apply_extrinsic(ext: <Block as BlockT>::Extrinsic) -> ApplyExtrinsicResult {
let signer = Self::verify_signed(ext.clone())?;
let dispatch_outcome = Self::apply_dispatch(&ext, signer);
Self::note_extrinsic(&ext);
// FIXME: Following can be left or removed from the assignment as we wish.
// Self::apply_predispatch(&ext, signer)?;
// let dispatch_outcome = Self::apply_dispatch(&ext, signer);
Ok(dispatch_outcome)
// todo!();
}
/// Your code path to execute a block that has been previously authored.
pub fn do_execute_block(block: Block) {
// clear any previous extrinsics. data.
// NOTE: Look into FRAME, namely the system and executive crates and see if this is any
// different in FRAME?
sp_io::storage::clear(&EXTRINSICS_KEY);
for extrinsic in block.clone().extrinsics {
let _outcome = Runtime::do_apply_extrinsic(extrinsic)
.expect("A block author has provided us with an invalid block; bailing; qed");
}
// check state root. Clean the state prior to asking for the root.
sp_io::storage::clear(&HEADER_KEY);
Self::print_state();
// NOTE: if we forget to do this, how can you mess with the blockchain?
let raw_state_root = &sp_io::storage::root(VERSION.state_version())[..];
let state_root = H256::decode(&mut &raw_state_root[..]).unwrap();
assert_eq!(block.header.state_root, state_root, "state root mismatch!");
// check extrinsics root
let extrinsics = Self::get_state::<Vec<Vec<u8>>>(EXTRINSICS_KEY).unwrap_or_default();
let extrinsics_root = BlakeTwo256::ordered_trie_root(extrinsics, Default::default());
assert_eq!(block.header.extrinsics_root, extrinsics_root);
info!(target: LOG_TARGET, "Finishing block import.");
}
/// Your transaction pool validation.
pub fn do_validate_transaction(
_source: TransactionSource,
ext: <Block as BlockT>::Extrinsic,
_block_hash: <Block as BlockT>::Hash,
) -> TransactionValidity {
// FIXME: Following can be left or removed from the assignment as we wish.
let signer = Self::verify_signed(ext.clone())?;
// let valid = Self::validate_nonce(&ext, signer)?;
// Self::validate_tip(&ext, signer)?;
Ok(ValidTransaction::default())
// todo!();
}
}
// This impl block contains some "candidate" functions that you could use to implement the denoted
// `FIXME` points above.
#[allow(unused_variables, dead_code)]
impl Runtime {
/// Receive the initial block header stored in `initialize_block`, and update its `state_root`
/// and `extrinsics_root`.
fn update_header(initial_header: Header) -> Header {
let state_root = {
let raw = &sp_io::storage::root(Default::default())[..];
H256::decode(&mut &raw[..]).unwrap()
};
let extrinsics = sp_io::storage::get(EXTRINSICS_KEY)
.and_then(|bytes| <Vec<Vec<u8>> as Decode>::decode(&mut &*bytes).ok())
.unwrap_or_default();
let expected_extrinsics_root =
BlakeTwo256::ordered_trie_root(extrinsics, Default::default());
let mut header = initial_header;
header.extrinsics_root = expected_extrinsics_root;
header.state_root = state_root;
header
}
/// Verify the extrinsic is properly signed and return the signing account if successful.
///
/// #### Errors
///
/// If no `extrinsic.signature` is present or if signature is not valid, return the error
/// [`sp_runtime::transaction_validity::InvalidTransaction::BadProof`].
fn verify_signed(
extrinsic: <Block as BlockT>::Extrinsic,
) -> Result<AccountId, TransactionValidityError> {
let (signer, signature, extra) = extrinsic.signature.ok_or(TransactionValidityError::Invalid(sp_runtime::transaction_validity::InvalidTransaction::BadProof))?;
let payload = extrinsic.function;
let success = signature.verify(payload.encode().as_ref(), &signer);
if success {
Ok(signer)
} else {
Err(TransactionValidityError::Invalid(sp_runtime::transaction_validity::InvalidTransaction::BadProof))
}
// todo!()
}
/// Perform the predispatch checks and tasks, namely
///
/// * check and update nonce
/// * collect tip from the signer account
///
/// If any of these actions fail, no changes should be made to the state and the correct error
/// is returned.
///
/// ## Nonce
///
/// You need to check nonce supplied in the extrinsic against the nonce stored in the signer's
/// account.
///
/// #### Errors
///
/// [`Future`](`sp_runtime::transaction_validity::InvalidTransaction::Future`) or
/// [`Stale`](sp_runtime::transaction_validity::InvalidTransaction::Stale) if the transaction's
/// nonce is not correct.
///
/// ## Collect Tip
///
/// Collect tip if [`shared::RuntimeCallExt::tip`] is `Some(_)`. The tip is transferred to
/// treasury.
///
/// IMPORTANT: If paying the tip causes the signer's account existence to change (read more
/// about this in `ExistentialDeposit` section of [`shared::CurrencyCall`]), the predispatch
/// should fail and revert any changes it made (such as incrementing the Nonce).
///
/// #### Errors
///
/// [`sp_runtime::transaction_validity::InvalidTransaction::Payment`] if the extrinsic cannot
/// pay for its declared tip.
fn apply_predispatch(
extrinsic: &<Block as BlockT>::Extrinsic,
signer: AccountId,
) -> Result<(), TransactionValidityError> {
todo!()
}
/// Look at the `UncheckedExtrinsic.function.call` and dispatch it to the corresponding
/// variant of [`shared::RuntimeCall`].
fn apply_dispatch(
extrinsic: &<Block as BlockT>::Extrinsic,
signer: AccountId,
) -> Result<(), DispatchError> {
match extrinsic.function.call {
RuntimeCall::System(shared::SystemCall::Set { value }) => {
sp_io::storage::set(shared::VALUE_KEY, &value.encode());
Ok(())
},
_ => Err(DispatchError::Other("not implemented "))
}
// todo!()
}
/// ## Nonce validation
///
/// You should implement a nonce system, as explained as a part of the tx-pool lecture. In
/// short, the validation of each transaction should `require` nonce `(sender, n-1).encode()`
/// and provide `(sender, n).encode()`. All accounts are created with nonce 0. The first valid
/// transaction nonce is 0, which if successful, will set the account nonce to 1.
///
/// #### Errors
///
/// [`sp_runtime::transaction_validity::InvalidTransaction::Future`] or `Stale` if the
/// transaction's nonce is not correct.
///
/// If valid, the correct `provides` and `requires` should be set.
fn validate_nonce(
ext: &<Block as BlockT>::Extrinsic,
signer: AccountId,
) -> TransactionValidity {
todo!()
}
/// Verify that the sender is able to pay the tip without their accounts ending up `Destroyed`
/// or in `Invalid` state.
fn validate_tip(ext: &<Block as BlockT>::Extrinsic, signer: AccountId) -> TransactionValidity {
todo!()
}
/// Note an extrinsic because it has passed all the checks to make it apply-able.
fn note_extrinsic(ext: &<Block as BlockT>::Extrinsic) {
let mut current = sp_io::storage::get(EXTRINSICS_KEY)
.and_then(|bytes| <Vec<Vec<u8>> as Decode>::decode(&mut &*bytes).ok())
.unwrap_or_default();
current.push(ext.encode());
sp_io::storage::set(EXTRINSICS_KEY, current.encode().as_ref());
// todo!()
}
}
impl_runtime_apis! {
impl sp_api::Core<Block> for Runtime {
fn version() -> RuntimeVersion {
VERSION
}
fn execute_block(block: Block) {
info!(
target: LOG_TARGET,
"Entering execute_block block: {:?} (exts: {})",
block,
block.extrinsics.len()
);
// Be aware: In your local tests, we assume `do_execute_block` is equal to
// `execute_block`.
Self::do_execute_block(block)
}
fn initialize_block(header: &<Block as BlockT>::Header) {
info!(
target: LOG_TARGET,
"Entering initialize_block. header: {:?} / version: {:?}", header, VERSION.spec_version
);
// Be aware: In your local tests, we assume `do_initialize_block` is equal to
// `initialize_block`.
Self::do_initialize_block(header)
}
}
impl sp_block_builder::BlockBuilder<Block> for Runtime {
fn apply_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> ApplyExtrinsicResult {
info!(target: LOG_TARGET, "Entering apply_extrinsic: {:?}", extrinsic);
Self::do_apply_extrinsic(extrinsic)
}
fn finalize_block() -> <Block as BlockT>::Header {
let header = Self::do_finalize_block();
info!(target: LOG_TARGET, "Finalized block authoring {:?}", header);
header
}
fn inherent_extrinsics(_data: sp_inherents::InherentData) -> Vec<<Block as BlockT>::Extrinsic> {
Default::default()
}
fn check_inherents(
_block: Block,
_data: sp_inherents::InherentData
) -> sp_inherents::CheckInherentsResult {
Default::default()
}
}
impl sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block> for Runtime {
fn validate_transaction(
source: TransactionSource,
tx: <Block as BlockT>::Extrinsic,
block_hash: <Block as BlockT>::Hash,
) -> TransactionValidity {
log::debug!(target: LOG_TARGET,"Entering validate_transaction. tx: {:?}", tx);
Self::do_validate_transaction(source, tx, block_hash)
}
}
// You can safely ignore everything after this.
impl sp_genesis_builder::GenesisBuilder<Block> for Runtime {
fn create_default_config() -> Vec<u8> {
// ignore this.
let genesis = serde_json::json!({});
serde_json::to_string(&genesis)
.expect("genesis state should be convertible to json")
.into_bytes()
}
fn build_config(_config: Vec<u8>) -> sp_genesis_builder::Result {
Runtime::do_build_config()
}
}
impl sp_api::Metadata<Block> for Runtime {
fn metadata() -> OpaqueMetadata {
OpaqueMetadata::new(Default::default())
}
fn metadata_at_version(_version: u32) -> Option<OpaqueMetadata> {
Default::default()
}
fn metadata_versions() -> sp_std::vec::Vec<u32> {
Default::default()
}
}
impl sp_offchain::OffchainWorkerApi<Block> for Runtime {
fn offchain_worker(_header: &<Block as BlockT>::Header) {}
}
impl sp_session::SessionKeys<Block> for Runtime {
fn generate_session_keys(_: Option<Vec<u8>>) -> Vec<u8> {
Default::default()
}
fn decode_session_keys(
_: Vec<u8>,
) -> Option<Vec<(Vec<u8>, sp_core::crypto::KeyTypeId)>> {
Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::{AccountId, Balance, RuntimeCallExt};
use parity_scale_codec::Encode;
use shared::{Extrinsic, RuntimeCall, VALUE_KEY};
use sp_core::hexdisplay::HexDisplay;
use sp_io::TestExternalities;
use sp_keyring::AccountKeyring;
use sp_runtime::{
traits::Extrinsic as _,
transaction_validity::{InvalidTransaction, TransactionValidityError},
};
fn set_value_call(value: u32, nonce: u32) -> RuntimeCallExt {
RuntimeCallExt {
call: RuntimeCall::System(shared::SystemCall::Set { value }),
tip: None,
nonce,
}
}
fn unsigned_set_value(value: u32) -> Extrinsic {
let call = RuntimeCallExt {
call: RuntimeCall::System(shared::SystemCall::Set { value }),
tip: None,
nonce: 0,
};
Extrinsic::new(call, None).unwrap()
}
fn sign(call: RuntimeCallExt, signer: AccountKeyring) -> (Extrinsic, AccountId) {
let payload = call.encode();
let signature = signer.sign(&payload);
(Extrinsic::new(call, Some((signer.public(), signature, ()))).unwrap(), signer.public())
}
fn signed_set_value(value: u32, nonce: u32) -> (Extrinsic, AccountId) {
let call = set_value_call(value, nonce);
let signer = AccountKeyring::Alice;
sign(call, signer)
}
/// Return the list of extrinsics that are noted in the `EXTRINSICS_KEY`.
fn noted_extrinsics() -> Vec<Vec<u8>> {
sp_io::storage::get(EXTRINSICS_KEY)
.and_then(|bytes| <Vec<Vec<u8>> as Decode>::decode(&mut &*bytes).ok())
.unwrap_or_default()
}
/// Fund an account with `value` such that it can transact. This can be empty for now, but once
/// you implement the currency part, you need to use it.
fn fund_account(who: AccountId, amount: Balance) {
// todo!();
}
/// Get the balance of `who`, if it exists.
fn get_free_balance(who: AccountId) -> Option<Balance> {
None
// todo!();
}
/// Author a block with the given extrinsics, using the given state. Updates the state on
/// the fly (for potential further inspection), and return the authored block.
fn author_block(exts: Vec<Extrinsic>, state: &mut TestExternalities) -> Block {
let header = shared::Header {
digest: Default::default(),
extrinsics_root: Default::default(),
parent_hash: Default::default(),
number: 0, // We don't care about block number here, just set it to 0.
state_root: Default::default(),
};
state.execute_with(|| {
Runtime::do_initialize_block(&header);
drop(header);
let mut extrinsics = vec![];
for ext in exts {
match Runtime::do_apply_extrinsic(ext.clone()) {
Ok(_) => extrinsics.push(ext),
Err(_) => (),
}
}
let header = Runtime::do_finalize_block();
assert!(
sp_io::storage::get(HEADER_KEY).is_none(),
"header must have been cleared from storage"
);
let onchain_noted_extrinsics = noted_extrinsics();
assert_eq!(
onchain_noted_extrinsics,
extrinsics.iter().map(|e| e.encode()).collect::<Vec<_>>(),
"incorrect extrinsics_key recorded in state"
);
let expected_state_root = {
let raw_state_root = &sp_io::storage::root(Default::default())[..];
H256::decode(&mut &raw_state_root[..]).unwrap()
};
let expected_extrinsics_root =
BlakeTwo256::ordered_trie_root(onchain_noted_extrinsics, Default::default());
assert_eq!(
header.state_root, expected_state_root,
"block finalization should set correct state root in header"
);
assert_eq!(
header.extrinsics_root, expected_extrinsics_root,
"block finalization should set correct extrinsics root in header"
);
Block { extrinsics, header }
})
}
/// Import the given block
fn import_block(block: Block, state: &mut TestExternalities) {
state.execute_with(|| {
// This should internally check state/extrinsics root. If it does not panic, then we
// are gucci.
Runtime::do_execute_block(block.clone());
// double check the extrinsic and state root. `do_execute_block` must have already done
// this, but better safe than sorry.
assert_eq!(
block.header.state_root,
H256::decode(&mut &sp_io::storage::root(Default::default())[..][..]).unwrap(),
"incorrect state root in authored block after importing"
);
assert_eq!(
block.header.extrinsics_root,
BlakeTwo256::ordered_trie_root(
block.extrinsics.into_iter().map(|e| e.encode()).collect::<Vec<_>>(),
Default::default()
),
"incorrect extrinsics root in authored block",
);
});
}
#[test]
fn does_it_print() {
// runt this with `cargo test does_it_print -- --nocapture`
println!("Something");
}
#[test]
fn does_it_log() {
// run this with RUST_LOG=frameless=trace cargo test -p runtime does_it_log
sp_tracing::try_init_simple();
log::info!(target: LOG_TARGET, "Something");
}
#[test]
fn host_function_call_works() {
// this is just to demonstrate to you that you should always wrap any code containing host
// functions in `TestExternalities`.
TestExternalities::new_empty().execute_with(|| {
sp_io::storage::get(&VALUE_KEY);
})
}
#[test]
fn encode_examples() {
// demonstrate some basic encodings. Example usage:
//
// ```
// wscat -c 127.0.0.1:9944 -x '{"jsonrpc":"2.0", "id":1, "method":"state_getStorage", "params": ["0x123"]}'
// wscat -c ws://127.0.0.1:9944 -x '{"jsonrpc":"2.0", "id":1, "method":"author_submitExtrinsic", "params": ["0x123"]}'
// ```
let unsigned = Extrinsic::new_unsigned(set_value_call(42, 0));
let signer = sp_keyring::AccountKeyring::Alice;
let call = set_value_call(42, 0);
let payload = (call).encode();
let signature = signer.sign(&payload);
let signed = Extrinsic::new(call, Some((signer.public(), signature, ()))).unwrap();
println!("unsigned = {:?} {:?}", unsigned, HexDisplay::from(&unsigned.encode()));
println!("signed {:?} {:?}", signed, HexDisplay::from(&signed.encode()));
println!("value key = {:?}", HexDisplay::from(&VALUE_KEY));
}
// Basic tests related to step 0.
mod basics {
use super::*;
use crate::shared::EXISTENTIAL_DEPOSIT;
#[test]
fn signed_set_value_works() {
// A signed `Set` works.
let (ext, who) = signed_set_value(42, 0);
TestExternalities::new_empty().execute_with(|| {
fund_account(who, EXISTENTIAL_DEPOSIT);
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(noted_extrinsics().len(), 0);
Runtime::do_apply_extrinsic(ext).unwrap().unwrap();
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), Some(42));
assert_eq!(noted_extrinsics().len(), 1, "transaction should have been noted!");
});
}
#[docify::export]
#[test]
fn bad_signature_fails() {
// A poorly signed extrinsic must fail.
let signer = sp_keyring::AccountKeyring::Alice;
let call = set_value_call(42, 0);
let bad_call = set_value_call(43, 0);
let payload = (bad_call).encode();
let signature = signer.sign(&payload);
let ext = Extrinsic::new(call, Some((signer.public(), signature, ()))).unwrap();
TestExternalities::new_empty().execute_with(|| {
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(
Runtime::do_apply_extrinsic(ext).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::BadProof)
);
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(noted_extrinsics().len(), 0, "transaction should have not been noted!");
});
}
#[docify::export]
#[test]
fn unsigned_set_value_does_not_work() {
// An unsigned `Set` must fail as well.
let ext = unsigned_set_value(42);
TestExternalities::new_empty().execute_with(|| {
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(
Runtime::do_apply_extrinsic(ext).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::BadProof)
);
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(noted_extrinsics().len(), 0);
});
}
#[docify::export]
#[test]
fn validate_works() {
// An unsigned `Set` cannot be validated. Same should go for one with a bad signature.
let ext = unsigned_set_value(42);
TestExternalities::new_empty().execute_with(|| {
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
assert_eq!(
Runtime::do_validate_transaction(
TransactionSource::External,
ext,
Default::default()
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::BadProof)
);
assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), None);
});
}
#[docify::export]
#[test]
fn import_and_author_equal() {
// a few dummy extrinsics. The last one won't even pass predispatch, so it won't be
// noted.
let (ext1, _) = signed_set_value(42, 0);
let (ext2, _) = signed_set_value(43, 1);
let (ext3, alice) = signed_set_value(44, 2);
let ext4 = unsigned_set_value(45);
let mut authoring_state = TestExternalities::new_empty();
authoring_state.execute_with(|| {
fund_account(alice, EXISTENTIAL_DEPOSIT);
});
let block = author_block(vec![ext1, ext2, ext3, ext4], &mut authoring_state);
authoring_state
.execute_with(|| assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), Some(44)));
let mut import_state = TestExternalities::new_empty();
import_state.execute_with(|| {
fund_account(alice, EXISTENTIAL_DEPOSIT);
});
import_block(block, &mut import_state);
import_state
.execute_with(|| assert_eq!(Runtime::get_state::<u32>(VALUE_KEY), Some(44)));
}
}
// some sanity tests for your currency impl
mod currency {
use super::*;
use crate::shared::{CurrencyCall, EXISTENTIAL_DEPOSIT};
#[test]
fn transfer_works() {
let call =
RuntimeCallExt::new_from_call(RuntimeCall::Currency(CurrencyCall::Transfer {
dest: AccountKeyring::Bob.public(),
amount: 42,
}));
let signer = AccountKeyring::Alice;
let (ext, alice) = sign(call, signer);
TestExternalities::new_empty().execute_with(|| {
fund_account(alice, 100);
assert_eq!(Runtime::do_apply_extrinsic(ext), Ok(Ok(())));
assert_eq!(get_free_balance(alice), Some(100 - 42));
assert_eq!(get_free_balance(AccountKeyring::Bob.public()), Some(42));
});
}
#[test]
fn mint_works_for_alice() {
let call = RuntimeCallExt::new_from_call(RuntimeCall::Currency(CurrencyCall::Mint {
amount: 42,
dest: AccountKeyring::Bob.public(),
}));
let signer = AccountKeyring::Alice;
let (ext, alice) = sign(call, signer);
TestExternalities::new_empty().execute_with(|| {
fund_account(alice, EXISTENTIAL_DEPOSIT);
assert_eq!(Runtime::do_apply_extrinsic(ext), Ok(Ok(())));
assert_eq!(get_free_balance(AccountKeyring::Bob.public()), Some(42));
});
}
#[test]
fn bad_transfer_fails() {
let call =
RuntimeCallExt::new_from_call(RuntimeCall::Currency(CurrencyCall::Transfer {
dest: AccountKeyring::Bob.public(),
amount: 120,
}));
let signer = AccountKeyring::Alice;
let (ext, alice) = sign(call, signer);
TestExternalities::new_empty().execute_with(|| {
fund_account(alice, 100);
assert!(matches!(
Runtime::do_apply_extrinsic(ext),
Ok(Err(DispatchError::Token(_)))
));
assert_eq!(get_free_balance(alice), Some(100));
assert_eq!(get_free_balance(AccountKeyring::Bob.public()), None);
});
}
}
mod tipping {
use super::*;
use crate::shared::{CurrencyCall, TREASURY};
use sp_core::crypto::UncheckedFrom;
#[test]
fn tipped_transfer_works() {
let call = RuntimeCallExt {
call: RuntimeCall::Currency(CurrencyCall::Transfer {
dest: AccountKeyring::Bob.public(),
amount: 42,
}),
tip: Some(10),
nonce: 0,
};
let signer = AccountKeyring::Alice;
let (ext, alice) = sign(call, signer);
TestExternalities::new_empty().execute_with(|| {
fund_account(alice, 100);
assert_eq!(Runtime::do_apply_extrinsic(ext), Ok(Ok(())));
assert_eq!(get_free_balance(alice), Some(100 - 42 - 10));
assert_eq!(get_free_balance(AccountId::unchecked_from(TREASURY)), Some(10));
assert_eq!(get_free_balance(AccountKeyring::Bob.public()), Some(42));
});
}
}
mod nonce {
use super::signed_set_value;
use crate::{shared::EXISTENTIAL_DEPOSIT, tests::fund_account, Runtime};
use sp_io::TestExternalities;
use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError};
#[test]
fn bad_nonce_fails_apply() {
TestExternalities::new_empty().execute_with(|| {
// first correct nonce is 0.
let (ext, who) = signed_set_value(42, 0);
fund_account(who, EXISTENTIAL_DEPOSIT);
assert!(matches!(Runtime::do_apply_extrinsic(ext), Ok(Ok(_))));
// next correct one is 1
let (ext, _) = signed_set_value(42, 0);
assert!(matches!(
Runtime::do_apply_extrinsic(ext),
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
));
let (ext, _) = signed_set_value(42, 2);
assert!(matches!(
Runtime::do_apply_extrinsic(ext),
Err(TransactionValidityError::Invalid(InvalidTransaction::Future))
));
})
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment