Noir Tracer

A useful tool when debugging Noir programs is one that can generate an execution trace of the program for the given inputs. Such a trace can be loaded in a specialized program that visualizes the execution timeline and gives insights into the behavior of the program during execution.

At Blocksense, we have built such a tool and integrated it in the nargo command line interface. Note that the project is still an early-alpha prototype, so there is still much to improve. Yet, it is already powerful enough to handle a broad range of Noir programs.

We are using the runtime tracing Rust library for the tracing format. Thanks to this, we have enabled the use of the time traveling debugger CodeTracer. With it you may traverse the execution timeline forwards and backwards in an intuitive GUI. More details about it can be found in the subpage Debugging with CodeTracer.

Installation

As already mentioned, the tool is integrated in the nargo CLI, but you need to fetch our fork of the Noir programming language in order to get it. Follow the installation guide for the Noir PLONKY2 backend, as it describes how to fetch the fork and build the system. Once you have nargo set up, you can use it in the following way.

Using

In order to generate a trace for a Noir program, you need to navigate to its root directory (the directory containing Nargo.toml). You also need to specify the directory where the trace should be generated.

You could try one of the programs included in test_programs. Alternatively, you can create a new one in the usual way:

nargo new simple_loop
cd simple_loop

Once that's done, write some text for the program (in src/main.nr):

fn main(x: pub u64, y: u64) {
    let mut z: u64 = y;
    for _ in 0..3 {
        z *= y;
    }
    assert(x % z == 0);
}

Generate a Prover.toml using nargo check.

nargo check

This command will generate the following Prover.toml:

x = ""
y = ""

Edit Prover.toml so that it contains an input for which the program will work:

x = "3072"
y = "4"

At this point, you can run nargo prove and nargo verify to check that the program is valid using our PLONKY2 backend. You can also execute it via nargo execute or run it in a debugger via nargo debug which are both upstream features.

That said, this page is about nargo trace. Using this new command, you can generate a recording (a trace) of the execution of the program and save that in a file. In order for this to work, you need to specify a directory where the recording should be stored via the --trace-dir command line parameter.

mkdir traces
nargo trace --trace-dir traces

The traces directory will now contain three files, two of which are metadata. The main file containing the trace is trace.json. You could inspect the files as is, but they are optimized for minimal whitespace, so you can use another convenience command we have added to the nargo CLI: nargo format-trace.

nargo format-trace traces/trace.json traces/formatted_trace.json

This generates the traces/formatted_trace.json file, which at the time of writing of this document looks like this:

[
  { "Type": { "kind": 30, "lang_type": "None", "specific_info": { "kind": "None" } } },
  { "Path": "<relative-to-this>/src/main.nr" },
  { "Function": { "path_id": 0, "line": 1, "name": "main" } },
  { "Type": { "kind": 7, "lang_type": "u64", "specific_info": { "kind": "None" } } },
  { "VariableName": "x" },
  { "VariableName": "y" },
  { "Call": { "function_id": 0, "args": [
    { "variable_id": 0, "value": { "kind": "Int", "i": 3072, "type_id": 1 } },
    { "variable_id": 1, "value": { "kind": "Int", "i": 4, "type_id": 1 } }
  ] } },
  { "Step": { "path_id": 0, "line": 2 } },
  { "Value": { "variable_id": 0, "value": { "kind": "Int", "i": 3072, "type_id": 1 } } },
  { "Value": { "variable_id": 1, "value": { "kind": "Int", "i": 4, "type_id": 1 } } },
  { "Step": { "path_id": 0, "line": 4 } },
  { "Value": { "variable_id": 0, "value": { "kind": "Int", "i": 3072, "type_id": 1 } } },
  { "Value": { "variable_id": 1, "value": { "kind": "Int", "i": 4, "type_id": 1 } } },
  { "VariableName": "z" },
  { "Value": { "variable_id": 2, "value": { "kind": "Int", "i": 4, "type_id": 1 } } },
  { "Step": { "path_id": 0, "line": 6 } },
  { "Value": { "variable_id": 0, "value": { "kind": "Int", "i": 3072, "type_id": 1 } } },
  { "Value": { "variable_id": 1, "value": { "kind": "Int", "i": 4, "type_id": 1 } } },
  { "Value": { "variable_id": 2, "value": { "kind": "Int", "i": 256, "type_id": 1 } } },
  { "Step": { "path_id": 0, "line": 7 } },
  { "Value": { "variable_id": 0, "value": { "kind": "Int", "i": 3072, "type_id": 1 } } },
  { "Value": { "variable_id": 1, "value": { "kind": "Int", "i": 4, "type_id": 1 } } },
  { "Value": { "variable_id": 2, "value": { "kind": "Int", "i": 256, "type_id": 1 } } }
]

Do note that this tool is at an early-alpha stage and the output is likely to change. That said, this guide should have given you an idea of how it is supposed to behave.

Specifically, from this trace, we can see that the trace contains all the steps that are taken during the execution of the program, as well as the contents of all the variables live at each step. Again, this is an early prototype and we are in the process of heavily optimizing it, including smart tracking of variables and reducing unnecessary repetition so that we keep the file size to a minimum.