Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Installation

The extraction tool needs to be built from source. This is because the harness definitions depend on the circuits crate in midnight-zk.

Once inside of the directory where the source is located you can build and run the tool.

First, verify that the version of midnight-circuits that the tool is linked against is the version you are interesting in extracting from.

Check that the dependency is pointing to the right repository and the right revision. Also make sure that it has the extraction feature enabled. Without this feature the circuits do not include the necessary integrations.

For example:

$ cargo info midnight-circuits
midnight-circuits
version: 5.0.1 (from https://github.com/Veridise/midnight-zk#0d63a4d4)
license: unknown
rust-version: unknown
features:
 +default              = []
  bench-internal       = [midnight-proofs/bench-internal]
  extraction           = [extractor-support, testing, midnight-proofs/extraction, picus/extractor-derive]
  extractor-support    = [dep:extractor-support]
  heap_profiling       = []
  serde                = [dep:serde]
  serde_derive         = [dep:serde_derive]
  serde_json           = [dep:serde_json]
  testing              = [num-bigint/rand]
  truncated-challenges = [midnight-proofs/truncated-challenges]

Once everything looks good you can build the tool as usual with cargo build --release.

Extraction

Once the tool is build you can use the midnight-extractor CLI to extract the circuits.

Quickstart

To extract all the circuits you can use the extract-all.sh script located in the scripts directory. The Picus representation of the extracted circuits can be found in the picus_files directory. By default this tool does not extract the LLZK versions.

CLI usage

The midnight-extractor CLI builds a list of extractable circuits at runtime then extracts them to the desired format. The CLI has a series of flags and arguments that allows controlling the selection of circuits to extract. The CLI also has flags for controlling the output produced by the tool.

Use the --help flag to see the full list of options.

Circuit selection

Each circuit has 4 properties that are used for filtering and selection; instruction, chip, type, and name.

The instruction groups together circuits that are related and in general corresponds to an instruction-like trait (i.e. arithmetic corresponds to ArithInstructions or ecc to EccInstructions). The instructions are selected as positional arguments. Adding instructions to the CLI invocation creates a set of selected instructions. If no instructions are passed to the CLI then all instructions are considered.

Possible instruction values: arithmetic, assertion, assignment, automaton, base64, base64-var, biguint, binary, bitwise, canonicity, committed-instance, comparison, control-flow, conversion, core-decomposition, decomposition, division, ecc, equality, field, foreign-ecc, hash, hash-to-curve, map, map-to-curve, parser, pow2-range, public-input, range-check, sha256, sponge, stdlib, unsafe-conversion, varhash, vector, zero.

The chip declares what concrete implementation of the instruction is used by the circuit. Different chips implement the same instruction traits and this parameter allows configuring what chip is going to be targeted. To select a chip use the --chip <name> flag. This will make the tool filter out any circuit that is not implemented by the chip. If the flag is not passed then the tool will consider all chips.

Possible chip values: native, native-gadget, field, poseidon, pow2-range, p2r-decomposition, sha256, ecc, foreign-ecc-native, foreign-ecc-field, vector, biguint, stdlib, automaton, base64, hash-to-curve, map, parser, varlen-poseidon, varlen-sha256.

The type declares what high-level type the circuit is using and can be selected with the --type <name> flag. Similarly to --chip, not passing this flag makes the tool consider all types. The circuits distinguish by type because some circuits implement the same instructions for multiple. For example, the native chip implements the equality instructions for the native and bit types.

Possible type values: native, bit, field, byte, biguint, scalar, point.

The name describes the functionality the circuit is trying to represent. In general corresponds to methods in one of the instruction-like traits. Some methods have a variable number of arguments. In cases like that then multiple circuits may be created with different combinations of arguments and the name will contain information about the combination (i.e. the circuits hash/hash_1/sha256/byte and hash/hash_10/sha256/byte operate on 1 and 10 input values respectively).

You can filter by name with both a whitelist and a blacklist. If the whitelist is passed, any circuit outside of the list is discarded. And, if the blacklist is passed, then any circuit inside it is discarded. If the whitelist or the blacklist are not set then they have no effect and all circuit names will be included in the extraction. Both lists can be configured as a comma separated list passed as arguments to the --method-whitelist and --method-blacklist flags.

You can combine these flags in any way you want. You can also pass the --list flag to the tool makes it print the selected circuits instead of extracting them. This is useful for debugging a circuit selection that is not producing the desired results.

Constants

Some harnesses require a list of literal values that will be used as compile-time constants representing off-circuit values in the harness’ input. To pass constants use either --constants or --constants-file. The former expects a comma separated list of literal values. All types expect their decimal representation with the exception of bit with expectes either true or false. The latter expects a path to a file containing lines of comma separated values. These values have the same requirements as the --constants flag.

If a harness requires more constants than supplied extraction will fail.

Output directory structure

The output directory can be selected with the -o flag and if the flag is omitted picus_files is used as a default. The tool writes into this directory one Picus file per extracted harness. These files are organized hierarchically by instruction, name, chip, and type. For example the harnesses for equality in the native chip will produce the following structure.

└── picus_files
    └── equality
        ├── is_equal_to
        │   └── native
        │       ├── bit
        │       │   └── output.picus
        │       └── native
        │           └── output.picus
        └── is_equal_to_fixed
            └── native
                ├── bit
                │   └── output.picus
                └── native
                    └── output.picus

AuditHub

AuditHub is a Veridise service where users can run verification tools such as Picus. Is accesible via both its web interface and CLI. In this document we will explain how to prepare a project for AuditHub, how to configure the CLI, and how to use it for interacting with AuditHub.

AuditHub CLI

The CLI is a Python 3 program that can be installed with pip (pip install audithub-client).

The CLI needs to be configured before using it. For that go to Account Settings / API Keys in AuditHub’s UI. Store the given client id and client secret somewhere secure depending on the intended usage of the keys.

API Key generation dialog

For using the keys locally it could be a .envrc file managed by direnv. For using them in CI that could be Github Secrets accesible by Github Actions.

Once the keys are ready check the CLI’s documentation on how to configure you system such that the CLI can use them to connect to AuditHub.

You can confirm that the CLI is working correctly by running the following

$ ah get-my-profile
{"id": "...", "name": "<Your name>", "email": "<Your email>", "rights": [...], ...}

Creating a new project

Before being able to upload new versions to AuditHub and launch jobs from the CLI it is necessary to create a project from the UI.

For the first version of the project generate at least some Picus files with midnight-extractor and pack them into a ZIP file.

# For example, generate some Picus files for the equality instructions
cargo run --release -- --chip native --type native equality
zip -r picus_files.zip picus_files

Then go to AuditHub’s UI and into the organization where you want to create the project and click in Create New Project

Organization dashboard

Given a name to the project and click Next.

Project Name

Select the File option and locate the ZIP file prepared earlier. Once you have it click Next.

Source Location

Select Archive root and click Next.

Project Root

In this section, leave this options as they are and click Next.

Project Paths

Select Picus files (and optionally LLZK files) and click Next.

Project Contents

Leave the next two sections untouched and just click Next.

Dependencies

Build

Check that everything looks right and click Submit.

Review

Once the project has been created you will be able to access its main view. For interacting with this project from the CLI you need to pass the organization and project IDs. Note in the part of the URL organizations/<number>/projects/<number>. The first is the organization ID and the second the project ID.

Project view

These IDs can be configured as environment variables and the CLI will use them while executing.

export AUDITHUB_ORGANIZATION_ID=<organization id>
export AUDITHUB_PROJECT_ID=<project id>

Uploading new versions

Make sure you went through the steps in Creating a new project before continuing.

After extracting a new version of the circuits but before running Picus with them, they need to be uploaded to AuditHub. This can be accomplised with either the UI or with the CLI. In this document we are going to see how to do it with the CLI, what you should have previously configured.

Each version needs to have a name. As an example we are going to use the date of submission and the sha256 of the generated picus files. From the shell run the following, assuming the picus files are in the default output folder (picus_files).

date=$(date "+%Y-%m-%d-%H-%M")
sum=$(find picus_files -type f | sort | xargs cat | sha256sum | head -c 8)
version_id=$(ah create-version-via-local-archive \
    --name "Picus-files-$date-$sum" \
    --source-folder picus_files \
    --project-id $PROJECT_ID \
    --organization-id $ORGANIZATION_ID)

If you configured the AUDITHUB_PROJECT_ID and AUDITHUB_ORGANIZATION_ID environment variables you can drop the --project-id and --organization-id flags since the CLI can read them from the environment. Note that we stored the output of the CLI in a variable named $version_id. We need this for identifying the version when launching jobs.

Launching jobs

Once you have uploaded a version of the extracted circuit to AuditHub you can start running tools with them.

We are going to see how to launch a Picus job from the CLI. Each individual Picus file needs to be run as a separate task. Pick one of the Picus files in the version you last uploaded and note its path relative to the directory you uploaded it from. For example, to run a job for up to one hour run the following in the shell.

# Example path that could be extracted 
PICUS_FILE=arithmetic/add/native/native/output.picus
ah start-picus-v2-task \
    --source $PICUS_FILE \
    --version-id $version_id \
    --project-id $PROJECT_ID \
    --organization-id $ORGANIZATION_ID \
    --time-limit 3600000 \
    --empty-assume-deterministic \
    --wait

This will wait until the job has completed in AuditHub. If the --wait flag is dropped the CLI returns immediately, responding with the task ID. This ID can be passed to ah monitor-task -t $task_id for checking the status of the task.

You can run ah start-picus-v2-task --help to get more details about the possible flags and arguments.

Similarly to the upload, if you configured the AUDITHUB_PROJECT_ID and AUDITHUB_ORGANIZATION_ID environment variables you can drop the --project-id and --organization-id flags since the CLI can read them from the environment.

Picus requires a bit of configuration in some cases; if you passed the --prelude flag while extracting circuits with either spread or automaton then you need to pass additional flags.

PreludeAdditonal Picus flags
spread--assume-deterministic Spread,Unspread
automaton--assume-deterministic Automaton
None--empty-assume-deterministic

Writing new harnesses

Adding more harnesses is done in the crates/harnesses crate. The crate is organized by instructions and, in some cases, by chips or gadgets that implement the instructions.

Conceptually, a harness is a function that receives a chip, a layouter, some inputs and returns a Result. In reallity a harness is a function with a more abstract signature but that complexity is usually managed by a macro that allows for a more declarative approach. However, if the macro is not an option see the section below on how to write harnesses from scratch.

Writing declarative harnesses

A harnesses written declaratively has the following structure.

#![allow(unused)]
fn main() {
// The harnesses are NOT parametrized by a field.
use mdnt_extractor_core::fields::Blstrs as F;

// Sets the harness name of the function below.
#[entry("control-flow/select/native/native")]
// Marks the function as a harness
#[harness]
pub fn select_native(
    // The first argument is the chip. The only requirement is that the type is a &-reference.
    chip: &NativeChip<F>,
    // The second argument is the layouter. The actual layouter variable is not a 
    // `impl Layouter<F>`. This is syntactic sugar for a namesake variable that 
    // implements `midnight_proofs::circuit::Layouter`. So the result is the same.
    layouter: &mut impl Layouter<F>,
    // The third argument are the inputs. The type of the input must implement `LoadFromCells`.
    (cond, a, b): (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>),
    // Optional fourth argument that allows injecting additional IR for aiding verification.
    injected_ir: &mut InjectedIR<RegionIndex, Expression<F>>
    // The output must be a Result<T, E> where T is a type that implements `StoreIntoCells` and 
    // E is `midnight_proofs::plonk::Error`.
) -> Result<AssignedNative<F>, Error> {
    // Runs the target method.
    chip.select(layouter, &cond, &a, &b)
}
}

And that’s it. The list of harnesses is defined automatically at compile time and can be obtained by calling the mdnt_harnesses::harnesses function. The extractor uses that function for selecting what harnesses need to be extracted.

The macros do a lot of heavy lifting in converting this form to how the harnesses look internally. The #[entry("...")] macro registers the function in the list of harnesses. This registration is accomplised with mdnt_extractor_core::entry! and in some cases is actually better to use this macro instead of #[entry]. For example, a harness that is parametrized by a constant parameter for selecting the size of some arrays.

#![allow(unused)]
fn main() {
entry!("bar/example_10/foo/native", example::<10>);
entry!("bar/example_20/foo/native", example::<20>);
#[harness]
pub fn example<const N: usize>(
    chip: &FooChip<F>,
    layouter: &mut impl Layouter<F>,
    arr: [AssignedNative<F>; N]
) -> Result<AssignedNative<F>, Error> {
    chip.foo(layouter, &arr)
}
}

The harness macro family has 6 macros that can be used for declaring harnesses and offer some flexibility for covering most cases.

For most chips (the ones where ChipArgs::Args == ()) use the macros harness, harness_mut, and unit_harness. The first macro is the one we saw above already. harness_mut is similar to harness but the first argument (the chip argument) must be a &mut-reference instead. unit_harness is for methods that do have a return value (Result<(),Error>). For this macro split the inputs of the function into two sets and pass them as the 3rd and 4th argument. The 4th argument can be considered the output and additional constraints can be injected for aiding Picus with verification. That argument, however, is not encoded as a Picus output. Is just a separation for readability. In these cases a vacuous output is generated that is constrained to be equal to 0.

If for the target chip ChipArgs::Args != () then you need to use harness_with_args, harness_with_args_mut, and unit_harness_with_args. These macros work identically to their counterparts but require declaring the type of ChipArgs::Args (i.e. #[harness_with_args(usize)]). For providing the argument you need to define a function that has the same name as the harness function followed by _args (i.e. foo would be foo_args). That function cannot take any arguments and return a value of the declared type.

Since the most common argument type is usize we include a convenience macro #[usize_args(<usize>)] that automatically creates the function with the correct name and returns the value passed as argument to the macro.

All the macros accept an optional argument with an expression containing LookupCallbacks. For example, for extracting a harness like the first one but for NativeGadget instead we need to handle the range lookup that gadget uses. For that we can do as follows:

#![allow(unused)]
fn main() {
#[entry("control-flow/select/native-gadget/native")]
// The range_lookup function creates the appropiate handler for the lookup.
#[harness(range_lookup(8))]
pub fn select_native(
    chip: &NG<F>,
    layouter: &mut impl Layouter<F>,
    (cond, a, b): (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>),
) -> Result<AssignedNative<F>, Error> {
    chip.select(layouter, &cond, &a, &b)
}
}

Writing harnesses from scratch (WIP)

If the macros shown above don’t fit the needs of this new harness they can still be defined by hand. Below is an annotated version of the kind of code the macros generate that can serve as a starting point for creating a harness from scratch.

#![allow(unused)]
fn main() {
entry!("control-flow/select/native-gadget/native", select_native);
fn select_native(ctx: &Ctx) -> anyhow::Result<Output> {
    // The actual logic of the harness needs to be wrapped into a trait.
    // So we need to create a type for that.
    struct Circuit<'s, 'c>(PhantomData<(&'s (), &'c ())>);

    // This trait defines the types that the harness uses. 
    // The extractor will rely on this trait for finding the right types for 
    // creating the right final circuit.
    impl<'s, 'c> AbstractCircuitIO for Circuit<'s, 'c> {
        // The chip type. It must implement CircuitInitialization but is not enforced.
        type Chip = NativeChip<F>;

        // The type of the inputs. Must implement `LoadFromCells` and if the method takes several 
        // they can be grouped in tuples up to 12 elements.
        type Input = (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>);

        // The type of the outputs. Same as the inputs but must implement the 
        // `StoreIntoCells` trait instead.
        type Output = AssignedNative<F>;

        // These two types need to be the same as their namesakes in CircuitInitialization.
        type Config = <NativeChip<F> as CircuitInitialization<ExtractionLayouter<'s, 'c, F>>>::Config;

        type ConfigCols =
            <NativeChip<F> as CircuitInitialization<ExtractionLayouter<'s, 'c, F>>>::ConfigCols;
    }

    // The actual logic is defined with this trait.
    // `harness_mut` will use `AbstractCircuitMut`
    impl AbstractCircuit<F> for Circuit<'_, '_> {
        fn synthesize<L>(
            &self,
            chip: &Self::Chip,
            layouter: &mut L,
            (cond, a, b): Self::Input,
            _: &mut InjectedIR<
                RegionIndex,
                Expression<F>,
            >,
        ) -> anyhow::Result<Self::Output, Error>
        where
            L: Layouter<F>,
        {
            chip.select(layouter, &cond, &a, &b)
        }
    }

    // If the chip arguments is () then this trait can be used.
    impl NoChipArgs for Circuit<'_, '_> {}

    // The type we created and implemented the types for can be passed to this 
    // type. This type handles the linking between the inputs, outputs, and the harness logic.
    let ci: CircuitImpl<'_, F, Circuit, Function> =
        CircuitImpl::new(ctx, Circuit(Default::default()));
    // The first argument of this method is a type that implements the  
    // CircuitSynthesis trait, which is Haloumi's counterpart to the Circuit trait in Halo2.
    // As long as the value passed there meets the interface all the stuff above this line is not 
    // mandatory.
    // The second argument is an optional dyn reference to a LookupCallbacks implementation.
    // These callbacks are invoked when the circuit has lookups for getting the IR that needs to be 
    // generated for handling the lookup.
    ctx.lower_circuit(ci, None)
}

}

Supporting new chips

In order to use user defined chips or gadgets in a harness it is necessary to implement a trait. This trait makes the chip or gadget compatible with the extraction system.

Let’s do a small refresher on why this is necessary. A harness can be defined as follows.

#![allow(unused)]
fn main() {
#[entry("control-flow/select/native/native")]
#[harness]
pub fn select_native(
    chip: &NativeChip<F>,
    layouter: &mut impl Layouter<F>,
    (cond, a, b): (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>),
) -> Result<AssignedNative<F>, Error> {
    chip.select(layouter, &cond, &a, &b)
}
}

This harness targets the NativeChip. When the extractor attempts to extract the harness it needs to know some things about this chip, but it doesn’t know what the NativeChip is. This information is obtained via the CircuitInitialization trait. This trait, similar in nature to Halo2’s Circuit trait, enables the extractor to configure and create chips that implement it.

Let’s consider the following chip as an example.

#![allow(unused)]
fn main() {
struct FooConfig {
    advice: [Column<Advice>; ADVICE_WIDTH],
    fixed: [Column<Fixed>; FIXED_WIDTH],
    table: [TableColumn; TABLE_WIDTH]
}

struct FooChip<F> {
    config: FooConfig,
    native: NativeChip<F>
}

impl<F: PrimeField> FooChip<F> {
    fn configure(meta: &mut ConstraintSystem<F>) -> FooConfig {
        // Create an instance of the config... 
    }

    fn new(config: FooConfig, native: NativeChip<F>) -> Self {
        Self { config, native }
    }

    fn load_table(&self, layouter: impl Layouter<F>) -> Result<(), Error> {
        // Loads the lookup table...
    }
}
}

Our example chip has a lookup table and depends on NativeChip, which already implements the trait we are going to implement. The trait is parametrized in L, that we can use as an implementation of a Layouter.

#![allow(unused)]
fn main() {
impl<F, L> CircuitInitialization<L> for FooChip<F>
where
    F: PrimeField,
    L: Layouter<F>,
{
    // Type that is generated from the ConstraintSystem, usually the Chip's config.
    type Config = (FooConfig, <NativeChip<F> as CircuitInitialization<F>>::Config);

    // Any external arguments the chip may need. If the chip does not have any then 
    // a 0-tuple is sufficient.
    type Args = ();

    // Any type that is part of the configuration that could be potentially shared with other chips.
    // This should have any type, potentially created by the ConstraintSystem, but that is not created by the chip itself.
    // FooChip does not have any of that in this case so we just put NativeChip's.
    type ConfigCols = <NativeChip<F> as CircuitInitialization<F>>::ConfigCols;

    // These two types are required because mdnt-support does not have a dependency on any 
    // halo2 library. We need to declare what types are used as ConstraintSystem and as Error.
    // Usually these will be the types `<halo2-crate>::plonk::{ConstraintSystem, Error}`.
    type CS = ConstraintSystem<F>;
    type Error = Error;

    /// Initializes a new chip with the given config and arguments, if any.
    fn new_chip((config, native_config): &Self::Config, _: Self::Args) -> Self {
        Self::new(config, NativeChip::new_chip(native_config, ()))
    }

    /// Creates a new configuration.
    fn configure_circuit(
        meta: &mut Self::CS,
        config_cols: &Self::ConfigCols,
    ) -> Self::Config {
        (Self::configure(meta), NativeChip::configure_circuit(meta, config_cols))
    }

    /// Performs any required loading, such as lookup tables.
    /// This method is called by the extractor after the harness' method has been executed.
    fn load_chip(&self, layouter: &mut L, (_, native_config): &Self::Config) -> Result<(), Self::Error> {
        self.load_table(layouter)?;
        self.native.load_chip(layouter, native_config)
    }
}

}

With this trait implemented the type can now be used as the chip argument in any harness.

Supporting new assigned types

In order to use user defined types as inputs or outputs in a harness it is necessary to implement some traits. These traits make the type compatible with the extraction system.

Let’s do a small refresher on why this is necessary. A harness can be defined as follows.

#![allow(unused)]
fn main() {
#[entry("control-flow/select/native/native")]
#[harness]
pub fn select_native(
    chip: &NativeChip<F>,
    layouter: &mut impl Layouter<F>,
    (cond, a, b): (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>),
) -> Result<AssignedNative<F>, Error> {
    chip.select(layouter, &cond, &a, &b)
}
}

In this example the type (AssignedBit<F>, AssignedNative<F>, AssignedNative<F>) is the type of the input and AssignedNative<F> is the type of the output. When lowering to Picus we cannot use this types directly because PCL does not support user defined types and everything is a felt. The extractor works around this issue by creating two auxiliary tables (one for inputs and another for outputs) that represent the inputs and outputs of the circuit as individual cells. In the example above we need 3 cells in the input auxiliary table and 1 cells in the output auxiliary table. We know this because AssignedNative<F> is an alias over Halo2’s AssignedCell<F,F> and AssignedBit<F> wraps an AssignedNative<F>.

As a running example, we are going to add support to the following type as we explain the concepts.

#![allow(unused)]
fn main() {
struct AssignedFoo<F> {
    foo: [AssignedNative<F>; 10],
    bar: AssignedBit<F>
}
}

In order for the extractor to know how many cells a type occupies we have the CellReprSize trait from the mdnt-support crate. This trait has a constant usize that declares the amount of cells required. Types such as AssignedCell<F,F> or [T;N] already implement this trait, so we can leverage that for implementing the trait for AssignedFoo.

#![allow(unused)]
fn main() {
// The impl of this trait for AssignedCell shows that it, as expected, only takes up one cell.
impl<V, F> CellReprSize for AssignedCell<V, F> {
    const SIZE: usize = 1;
}

// For our type we can leverage the existing implementations.
impl<F> CellReprSize for AssignedFoo<F> {
    const SIZE: usize = <[AssignedNative<F>; 10]>::SIZE + <AssignedBit<F>>::SIZE; // Total 11 cells.
}
}

This trait requires that the type has a known size and won’t work well with types that have dynamically sized types such as Vec. The following pattern can be used to work around this limitation.

#![allow(unused)]
fn main() {
/// Dynamically sized version of AssignedFoo
struct AssignedFooDyn<F> {
    foo: Vec<AssignedNative<F>>,
    bar: AssignedBit<F>
}

/// We create a new type that has a known length.
struct LoadedFooDyn<F, const N: usize> {
    foo: AssignedFooDyn<F>,
    _marker: PhantomData<[(); N]>
}

impl<F, const N: usize> From<LoadedFooDyn<F, N>> for AssignedFooDyn<F> {
    fn from(value: LoadedFooDyn<F, N>) -> Self {
        value.foo
    }
}

// And implement the trait on this new type wrapper. The other traits that we will see 
// below need to be implemented on this trait as well.
impl<F, const N: usize> CellReprSize for LoadedFooDyn<F, N> {
    const SIZE: usize = <AssignedNative<F>>::SIZE * N + <AssignedBit<F>>::SIZE; // Total N+1 cells.
}
}

Now that we have the trait that declares the size of the type we can add support for reading, writing, or both. For using the type as an input in a harness we need to implement LoadFromCells. And for using the type as an output we need to implement StoreIntoCells. Both traits have 4 type parameters: F, C, E, and L. F is the type of the field and needs to implement the ff::PrimeField trait. C is the chip we are using for the harness. In the example at the beginning of this chapter it would be NativeChip. E is the extraction adaptor, which is a type that has a set of associated types representing different Halo2 concepts. E must implement the Halo2Types trait in mdnt-support. Last, L does not necessarily need to implement any trait in order to satisfy either LoadFromCells or StoreIntoCells. However, some implementations may require an instance of a Layouter and L can be constrained to that trait (L: Layouter<F>).

#![allow(unused)]
fn main() {
impl<F, L, C, E> LoadFromCells<F, C, E, L> for AssignedFoo<F>
where 
    F: PrimeField,
    E: Halo2Types<F>
{
    fn load(
        ctx: &mut ICtx<F, E>,
        chip: &C,
        layouter: &mut impl LayoutAdaptor<F, E, Adaptee = L>,
        injected_ir: &mut InjectedIR<E::RegionIndex, E::Expression>,
    ) -> Result<Self, E::Error> {
        // Inside this method we need to construct an instance of the type.
        Ok(Self {
            // We can leverage the existing implementations of the trait.
            foo: ctx.load(chip, layouter, injected_ir)?,
            bar: ctx.load(chip, layouter, injected_ir)?,
        })
    }
}

impl<F, L, C, E> StoreIntoCells<F, C, E, L> for AssignedFoo<F>
where 
    F: PrimeField,
    E: Halo2Types<F>
{
    fn store(
        self,
        ctx: &mut OCtx<F, E>,
        chip: &C,
        layouter: &mut impl LayoutAdaptor<F, E, Adaptee = L>,
        injected_ir: &mut InjectedIR<E::RegionIndex, E::Expression>,
    ) -> Result<(), E::Error> {
        // Inside this method we need to convert the type into a set of cells.
        // Again, we can leverage existing implementations.
        self.foo.store(ctx, chip, layouter, injected_ir)?;
        self.bar.store(ctx, chip, layouter, injected_ir)
    }
}
}

Inside those traits the programmer has the ability of performing the conversion in different ways. The ctx gives access to the raw cells. The chip and layouter allow performing the conversion via some method the chip implements. For example, because the target chip implements a trait that already has the desired logic (i.e. adding C: ThatTrait to the where bounds). Lastly, injected_ir allows writing Haloumi IR directly for encoding semantic properties of the type. For example, the implementations for AssignedBit and AssignedByte in midnight-circuits emits IR that constraints the value to be within the range of the types; 0-1 and 0-255 respetively.