Skip to content

Commit

Permalink
Merge pull request #103 from w3f/substrate-section-7
Browse files Browse the repository at this point in the history
Intro to Substrate MOOC - Module 7
  • Loading branch information
DrW3RK authored Aug 3, 2023
2 parents 1c1df34 + 1ec1b2b commit 431a35e
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 3 deletions.
52 changes: 51 additions & 1 deletion docs/Substrate/section7/blockchain-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,54 @@ id: blockchain-dev
title: Thinking in terms of blockchain development
sidebar_label: Thinking in terms of blockchain development
description: How to think like a runtime developer and avoid common pitfalls.
---
---

If you have come from a web2 background, it can be a learning curve to "think like a blockchain developer".

While this course merely touches on the more surface-level capabilities of Substrate, there are some critical practices and concepts to remember when starting your journey to develop blockchains with Substrate.

## On Cryptography - "Don't roll your own crypto"

It is important to abide by the general rule of thumb of **"not rolling your own crypto"**. Use established or community-approved algorithms.

Whether it is apparent or not, all blockchain-based systems rely heavily on various cryptographic methods to track, verify, and provide the integrity blockchains intrisically offer.

As we've seen with the `StorageMap` implementation, we used the Blake2 hashing algorithm to hash our storage keys, or more important; we require an extrinsic to be a cryptographically valid, signed payload that represents that specific call. The blockchain itself relies on a series of hashes that build on each other to form the infamous, verifiable ledger that is the blockchain.

```mermaid
---
title: Blockchain 101 - Hashes
---
flowchart LR
subgraph Blocks
direction TB
b1c["B1 State Changes"] --> b2c["B2 State Changes"] --> b3c["B3 State Changes"] --> b4c["B4 State Changes"]
end
subgraph Ledger
direction TB
b1["Hash(B1)"] --> b2["Hash(B2)"] --> b3["Hash(B3)"] --> b4["Hash(B4)"]
end
b1 ==> b1c
b2 ==> b2c
b3 ==> b3c
b4 ==> b4c
Ledger ==> final["State Root Hash"]
```

Not using approved cryptography within your chain could make it fundamentally insecure and vulnerable to attack.

## On Storage - Blockchains aren't just databases

There is a common misconception that a blockchain is simply a distributed database; with that thinking, any data can be put on the blockchain. When developing a pallet or dApp, it is important to realize **do not store data, but rather a representation of it**.

:::info A real-world example: Video on blockchain

In this hypothetical scenario, a platform wishes to gate access to a video streaming platform unless certain NFTs are owned. Video is a computationally expensive to store and manage - so would the blockchain instance store each video?

**The answer is no.** In most cases, the blockchain merely **points** to the content but doesn't store it on the network.

:::

As we've seen, blockchains are not simply just a database - they represent an autonomous way to agree upon the state of some network. In order to agree on this state of the network, each node must hold some semblance of a copy of the network. In many cases, full nodes bear the brunt of the load when it comes to proving the state and providing access to it. Moreover, storing data on-chain is "expensive". While fees aren't an issue in our developmental network, a network like Polkadot requires a fee for a state change. The more data in that state change, the more computation and storage it will cost the network.

61 changes: 60 additions & 1 deletion docs/Substrate/section7/how-to-test-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,63 @@ id: how-to-test-frame
title: How to approach testing in FRAME
sidebar_label: How to approach testing in FRAME
description: How to correctly approach testing in FRAME and a deeper look at how to mock types properly.
---
---

In [Module 5](../section5/unit-tests.md), we briefly reviewed a basic unit test implementation for our pallet. This section will provide more depth to testing a pallet in Substrate and how to approach it in general.

## Good Practices - Test Everything

As discussed in [Runtime Panics & FRAME Best Practices](./runtime-panics.md), your pallet should never explicitly panic due to something like `unwrap()`-ing an invalid value or even accounting for edge cases like integer overflows by using [safe arithmetic](./runtime-panics.md#using-safe-arithmetic).

The same approach of ensuring **any** edge case is covered in the runtime can be reinforced in unit tests. Ideally, every error should have an accompanying (or inclusive) unit test to ensure the error is correctly handled.

## Mocking Types and Pallets

Unit tests require a mock runtime environment to be defined. Because using actual primitives could be prohibitive in testing, types are mostly mocked.

If you go back to the `substrate-mooc-node/pallets/connect/src/mock.rs` and observe the mock runtime called `Test`, `AccountId` is a prime example of a simple, mock primitive as `u64`:

```rust
impl frame_system::Config for Test {
...
type AccountId = u64;
...
}
```

An account id is simply a number versus a more complex type in testing. You may also notice many types in this configuration are merely just the Rust unit type, as they aren't relevant in the context of this mock environment (at least for our specific pallet) as denoted by `SomeConfigParam = ()`.

Other types, such as a `Block` within the runtime, are helpfully defined via `frame_system::mocking::MockBlock<Test>`.

### Mocked Pallets

Because pallets are external modules that expose several traits and their respective `struct`, they can also be configured via these mock types. Take a look at `pallet_balance`, which is defined within `mock.rs`:

```rust
impl pallet_balances::Config for Test {
type Balance = u128;
type RuntimeEvent = RuntimeEvent;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type ReserveIdentifier = [u8; 8];
}
```

Again, notice how `Balance` is defined as a primitive Rust `u128` type.

### Case Study: Randomness

For some other traits/pallets, it's possible that `frame_support_test` can provide extra mock and testing-related crates. For example, `TestRandomness<T>` is a mock type for randomness since we don't have a running chain to generate the traditional entropy. For this reason, it also makes our tests much more predictable (because it's a predictable source of randomness for testing), which is ideal:

```rust
/// These values will always be the same
#[test]
fn generate_gradient_with_correct_length() {
let hex = Connect::generate_hex_values(H256([0; 32]));
println!("{:?}", hex);
assert_eq!(hex.0, [8, 48, 48]);
assert_eq!(hex.1, [8, 48, 48]);
}
```

It's also possible to define your types for this substitution.
2 changes: 2 additions & 0 deletions docs/Substrate/section7/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Building a Custom FRAME Pallet - Pallet & FRAME best practices
---

In this module, you will learn more granular details and practices on ensuring your pallets are built using sound practices.

import DocCardList from '@theme/DocCardList';

<DocCardList />
32 changes: 31 additions & 1 deletion docs/Substrate/section7/runtime-panics.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,34 @@ id: runtime-panics
title: Runtime Panics & FRAME Best Practices
sidebar_label: Runtime Panics & FRAME Best Practices
description: Learn how to solidify safe programming practices using Rust error handling to avoid panicking in the runtime.
---
---

If there is one "rule of thumb" in developing in a runtime environment, is to **avoid runtime panics** at all costs. With the premise that a blockchain should never cease to operate, abusing the notion of `panic!`(king) in Rust should never be used.

## Avoiding runtime panics - Defensive Programming

Avoiding runtime panics is done by implementing some concepts of **defensive programming**. Here is a checklist of some common things to avoid and do in order to avoid panicking:

1. Account for any and all edge cases in terms of what *could* panic.
2. Do not use `panic!()`.
3. Do not use `unwrap()`.
4. Handle all possible `None` or `Err` values with proper error handling.
5. When indexing collections, use `Vec` methods like `get()` and handle appropriately.
6. Mathematically impossible operations, such as dividing by zero, or overflow scenarios.

In some cases, using `unwrap_or_default()` is appropriate, but only if a `Default` implementation exists for that particular type. Generally, you do not want to *throw* errors, rather you want to log and return them.

:::caution Logging has a cost in the runtime.

Keep in mind that logging has a computational cost in the runtime environment.

:::

### Using safe arithmetic

In the previous modules, we used a function - `checked_add()`. This is part of defensive programming, as we even negate the chance of an overflow of an integer. Substrate provides a number of ways to handle not only adding or subtracting numbers, but also floating point numbers (and subsequently, percentages).

### Accounting for all errors

As mentioned, every single error should be accounted for - even if logically, that error cannot be reached. This is part of ensuring that any functions that can panic `unwrap()`, have even a chance to panic. Whether it's for casting to a different type, user-induced errors, or the like - all branched paths should be covered.

0 comments on commit 431a35e

Please sign in to comment.