/frameless-node-template

Example and Exercises related to writing a runtime without FRAME

Primary LanguageRustThe UnlicenseUnlicense

Substrate Frameless Node Template

Welcome to the FRAME-LESS Runtime!

A stripped down version of the node template, ready for hackin'.

Instructions

This assignment has multiple parts. The ungraded part will be in the class and the TAs will help you with it.

Ungraded

1: Get Those Roots In Order!

The template already does most of the work to make sure your heads contain the right state root and extrinsic root. As your first step, make sure this is the case! Both of these should be set in on_finalize, and double-checked in execute_block.

The easy way to check that your extrinsic root check is working is running your node without --release. This will enable a debug_assert! on the client side that will ensure your authored block is setting the right extrinsic root.

Also, until you fix this, a second node, as explained below will not be able to sync your node.

The second node should fail to sync until you fix this. Why is that? because the given implementation only checks the extrinsic root at the end of execute_block.

Recall that the flow of a block-author node is:

initialize_block(empty_header: Header);
apply_extrinsic(ext: _);
// any changes made in this are not persisted in state.
finalize_block() -> Header;

and the block importer calls:

execute_block(actual_block: Block);

2. Basic write

Alter your BasicExtrinsic to accept a very basic call type where you write a given value into the storage under a hardcoded key.

Something along the lines of

pub enum Call {
	Set(u32),
}

2. Make it Upgradable!

Next, make your chain upgradable. This should in itself be really easy, it is almost like the previous set.

Whilst doing the upgrade, make sure to:

  1. break your transaction format. For example, change Set(u32) to Set(u128)
  2. bump your spec-version so that you won't get native execution issues.

Now, try and submit a new transaction...

3. Opaque Transactions

Yes, it failed because of:

pub mod opaque {
	type OpaqueExtrinsic = BasicExtrinsic;
	pub type Block = generic::Block<Header, OpaqueExtrinsic>;
}

This module is used in your client to understand what an "opaque" (untyped) extrinsic is, and you have hardcoded it to `BasicExtrinsic``! Of course, because your client is not upgraded, it cannot decode new transactions anymore (and if you send the old format, the runtime won't be able to decode it).

The correct type to use is sp_runtime::OpaqueExtrinsic. Take a look and replace it.

type OpaqueExtrinsic = sp_runtime::OpaqueExtrinsic;

Replay the above scenario. Will it work?

4. Opaque Decoding

Well, still no. The reason for that is that now your client will think of an extrinsic as Vec<u8>, and the Runtime thinks of it as BasicExtrinsic. How are you going to link the two?

Basically, ask yourself: how can you encode the BasicExtrinsic, such that it is decode-able as both BasicExtrinsic`` and Vec`?

In other words, imagine you pass in some bytes to your curl/wscat command. The same bytes should be decode-able as both a Vec<u8> and BasicExtrinsic.

This requires you to write a custom Encode/Decode implementation for your BasicExtrinsic. The best way to hint you at the solution is to guide you toward the type that is used in most real substrate-based chains: UncheckedExtrinsic: https://paritytech.github.io/substrate/master/sp_runtime/generic/struct.UncheckedExtrinsic.html. See why and how the Encode/Decode implementation for this type is different.

5. Timestamp

Next, write an inherent for your runtime that puts the timestamp into the block. For this, you need a new call type like

pub enum Call {
	Set(u32),
	SetTimestamp(u64),
	Upgrade(Vec<u8>),
}

The client will ask the runtime to create any given inherent at fn inherent_extrinsics and asks it to do any kind of soft-verification at fn check_inherent. In both cases, the substrate client will put its currently known timestamp at sp_inherent::INHERENT_IDENTIFIER key of data.

Look into pallet-timestamp for inspiration.

6. Optional: Get Block Author

Inside of the block header, there is a field called Digest. This contains some information that is passed from the client, to the runtime, as part of the header. Part of this information is about which authoring "engine" is being used, and which validator is authoring blocks. Extract this information, and stored it onchain, such that one can query a storage item from your chain state and know who authored a given block.

Look into pallet-authorship for inspiration.

Graded

TBD 😈

How To Build and Run

cargo b and cargo r (short for run and build) are your default ways to build and run the project. In almost all cases, you should run your node with --release to get a reasonable performance. If you only build, the binary can be found in ./target/{debug|release}/node-template.

Once the project has been built, the following command can be used to explore all parameters and subcommands:

./target/release/node-template -h

Running

This command will start the single-node development chain with non-persistent state:

./target/release/node-template --dev

--dev will imply multiple things:

  1. Set --alice as your local node authority, which means your node will author blocks (Alice is the block author of the dev chain by default).
  2. Set --tmp, which means every time your chain would start from scratch.

If you don't include --tmp, your chain will start accumulating its database. For this exercise, you probably want to stick to --tmp. In case you don't you can purge the development chain's state:

./target/release/node-template purge-chain --dev

Start the development chain with detailed logging:

In order to have all of the runtime logs enabled, run the project with RUST_LOG=frameless=trace.

RUST_LOG=frameless=trace ./target/release/node-template --dev

Or

./target/release/node-template --dev -lframeless=trace

Multiple-nodes

In order to run multiple nodes, you can try either of:

# runs a chain with --chain=dev --alice --tmp
$ ./target/release/node-template --dev
# runs a chain with --chain=dev --bob --tmp
$ ./target/release/node-template --dev --bob

RPC Cheatsheet

Consult the JSON-RPC lecture (including the speaker notes!), or:

# read a storage key/
wscat -c 127.0.0.1:9944 -x '{"jsonrpc":"2.0", "id":1, "method":"state_getStorage", "params": ["0x123"]}'

# submit an extrinsic
wscat -c 127.0.0.1:9944 -x '{"jsonrpc":"2.0", "id":1, "method":"author_submitExtrinsic", "params": ["0x123"]}'

# You can easily achieve the same with `curl` as well:

curl http://34.79.74.54:9934 -H "Content-Type:application/json;charset=utf-8" -d   '{"jsonrpc":"2.0","id":1,"method":"system_chain"}'

The easiest way to get the encoded keys is to write a unit test in your runtime that encodes a key/extrinsic.