1

In a substrate runtime, take the node-template as an example, we have two definitions from Block, among other types such as BlockId but let's focus on Block.. :

// runtime/src/lib.rs
pub type Block = generic::Block<Header, UncheckedExtrinsic>;

pub mod opaque {
 pub type Block = generic::Block<Header, UncheckedExtrinsic>;
 // -- snip --
}

Now, as per my understanding, these opaque::* types are used in the client side of things as a placeholder type to correctly check trait bounds, for instance, when instantiating an RPC server, you must make sure that your runtime has the correct RuntimeApis. For that you need this type information such as Block as demonstrated in the trait bound below :

// node/src/rpc.rs
fn create_full<_>() where ... // -- snip --
    C::Api: BlockBuilder<Block>, // Here, Block is runtime::opaque::Block
    { ... } 

and from what I see, this is generally imported as:

use runtime::opaque::Block;

A brief description of opaque types is given just above the opaque module as :

Opaque types. These are used by the CLI to instantiate machinery that don't need to know
the specifics of the runtime. They can then be made to be agnostic over specific formats
of data like extrinsics, allowing for them to continue syncing the network through upgrades
to even the core data structures.

My question:

Why use runtime::opaque::Block instead of runtime::Block ? If I understand correctly, opaque::Block and Block must be aliases to the same type in the runtime or else we couldn't make sure that our trait bounds for RuntimeApis would be satisfied in the node-client code. In other words, I don't quite understand the rationale behind Opaque types as pasted above.

1 Answer 1

2

The reason for this is exactly as the comment you quoted above the opaque module describes--agnosticity over runtime format changes.

To be more specific, runtime::opaque::Block and runtime::Block are not aliases to the same type. A block is a header (which does not change with WASM runtime upgrades) and extrinsics.

The Opaque Block

The relevant snippets of code are below:

// runtime/src/lib.rs
pub mod opaque {
  pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;
  
  /// Opaque block type.
  pub type Block = generic::Block<Header, UncheckedExtrinsic>;
}

// sp_runtime
/// Simple blob to hold an extrinsic without committing to its format and ensure it is serialized
/// correctly.
#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, TypeInfo)]
pub struct OpaqueExtrinsic(Vec<u8>);

The block is actually just a header, and a vector of bytes for the extrinsics.

The Runtime Block

The relevant snippets of code are:

// runtime/src/lib.rs
pub type Block = generic::Block<Header, UncheckedExtrinsic>;

// This is different!!!
pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<
  Address,
  RuntimeCall,
  Signature,
  SignedExtra
>;

// sp_runtime::generic
pub struct UncheckedExtrinsic<Address, Call, Signature, Extra> {
  pub signature: Option<(Address, Signature, Extra)>,
  pub function: Call,
}

In the runtime block, the extrinsic depends on the RuntimeCall, which is changed whenever any of the runtime logic changes! So this Block type looks very different than the opaque block.

Putting them together

Given that we have these very different structs, how do we use them interchangeably? Being able to use them together is a consequence of how they are encoded and decoded. generic::UncheckedExtrinsic has some special SCALE encoding and decoding logic so that it can losslessly be encoded to and decoded from a vector of u8s.

Because the Header type for both opaque::Block and Block are the same, and the Extrinsic type encodes/decodes the same, these blocks interoperate. In practice, extrinsics and blocks come in to the outer node (or client) of the substrate node as opaque extrinsics and blocks, which are just bytes. Then, they are passed into the WASM runtime by being encoded. Finally, they are decoded inside the runtime into the runtime extrinsic and block types, which can change with WASM upgrades.

2
  • This was an excellent answer. I didn't know about the SCALE encoding/decoding logic on UncheckedExtrinsic, as I was always scratching my head thinking in terms of "rust" types and briefly forgetting that the runtime compiles to WASM. This is very interesting, but however I've heard that the runtime can be executed as native code as well. In that case, do we also rely on SCALE encoding/decoding to ensure compatibility between Block type among others ? Commented May 25, 2023 at 6:11
  • I can't give a great answer on this, but I have some notes. The runtime can be executed as native, but there is discussion around it (which is a pretty good read). Runtime calls are always encoded (as of PR #12201), but it isn't for that reason. Using a native runtime, it doesn't matter if types are compatible, because a native runtime can only support a code version which it compiled from source. A client using WASM can run many versions just by downloading the new WASM, but a client using native must be restarted / recompiled. Commented May 25, 2023 at 17:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.