Validium Docs
  • Overview
  • Connect to Validium
  • Start Coding 🚀
    • Quickstart
      • Overview
      • Deploy using Validium CLI
      • Deploy using Quickstart Repository
      • Deploy your first contract
      • Create an ERC20 token
  • Tooling
    • Block Explorers
    • Hardhat-Validium
      • Overview
      • Installation
      • Guides
        • Getting started
        • Migrating Hardhat project to Validium
        • Compiling non-inlinable libraries
      • Plugins
        • hardhat-zksync
        • hardhat-zksync-solc
        • hardhat-zksync-vyper
        • hardhat-zksync-deploy
        • hardhat-zksync-upgradable
        • hardhat-zksync-verify
        • hardhat-zksync-verify-vyper
        • hardhat-zksync-ethers
        • hardhat-zksync-node
        • Hardhat Community Plugins
    • Foundary
      • Overview
      • Installation
      • Getting Started
      • Migration Guide
        • Overview
        • Compilation
        • Deployment
        • Testing
  • Test and Debug
    • Getting Started
    • Docker L1 - L2 Nodes
    • In-Memory Node
    • Continuous Integration
    • Hardhat
    • Foundry
  • API Reference
    • Overview
    • Conventions
    • ZKs JSON-RPC API
    • Debug JSON-RPC API
    • Ethereum JSON-RPC API
    • PubSub JSON-RPC API
  • Concepts
    • Transaction Lifecycle
    • Blocks and Batches
    • Validium Network Fee Mechanism
    • Finality
    • System Upgrades
    • ZK Chains
    • Data Availability
      • Overview
      • Recreating L2 state from L1 pubdata
      • Validiums
    • Account Abstraction
    • L1 <-> L2 Communication
  • Components
    • Overview
    • Smart & System Contracts
      • Smart Contracts
      • System Contracts
    • Shared Bridges
    • Sequencer / Server
    • Validium Network EVM
      • Overview
      • Bootloader
      • Precompiles
      • Virtual Machine Specification
        • ZKsync Virtual Machine primer
        • VM Formal Specification
    • Prover
      • Overview
      • ZK Terminology
      • Running the Prover
      • Circuits
        • Overview
        • Circuit Testing
        • CodeDecommitter
        • DemuxLogQueue
        • ECRecover
        • KeccakRoundFunction
        • L1MessagesHasher
        • LogSorter
        • Main VM
        • RAMPermutation
        • Sha256RoundFunction
        • StorageApplication
        • Sorting and Deduplicating
          • Overview
          • SortDecommitments
          • StorageSorter
          • LogSorter
      • Boojum Gadgets
      • Boojum Function - `check_if_satisfied`
    • Compiler
      • Compiler Toolchain Overview
        • Compiler Toolchain Overview
        • Solidity Compiler
        • Vyper Compiler
        • LLVM Framework
      • Specification
        • Overview
        • Code Separation
        • System Contracts
        • Exception Handling
        • EVM Legacy Assembly Translator
        • Instructions
          • Instruction Reference
          • EVM
            • Native EVM Instructions
            • Arithmetic
            • Bitwise
            • Block
            • Call
            • Create
            • Environment
            • Logging
            • Logical
            • Memory
            • Return
            • Sha3
            • Stack
          • Extensions
            • Overview
            • Validium Network Extension Simulation (call)
            • Validium Network Extension Simulation (verbatim)
          • EVM Legacy Assembly
          • Yul
        • EraVM Binary Layout
    • Fee Withdrawer
    • Portal - Wallet + Bridge
    • Block Explorer
    • Transaction filtering
Powered by GitBook
On this page
  • LogSorter PI
  • Main circuit logic
  1. Components
  2. Prover
  3. Circuits
  4. Sorting and Deduplicating

LogSorter

PreviousStorageSorterNextBoojum Gadgets

Last updated 8 months ago


LogSorter is one circuit that is used as both EventsSorter and L1MessagesSorter.

LogSorter PI

Input

pub struct EventsDeduplicatorInputData<F: SmallField> {
    pub initial_log_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
    pub intermediate_sorted_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
}

Output

pub struct EventsDeduplicatorOutputData<F: SmallField> {
    pub final_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
}

FSM Input and FSM Output

pub struct EventsDeduplicatorFSMInputOutput<F: SmallField> {
    pub lhs_accumulator: [Num<F>; DEFAULT_NUM_PERMUTATION_ARGUMENT_REPETITIONS],
    pub rhs_accumulator: [Num<F>; DEFAULT_NUM_PERMUTATION_ARGUMENT_REPETITIONS],
    pub initial_unsorted_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
    pub intermediate_sorted_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
    pub final_result_queue_state: QueueState<F, QUEUE_STATE_WIDTH>,
    pub previous_key: UInt32<F>,
    pub previous_item: LogQuery<F>,
}

Main circuit logic

The main logic of this circuit is sorting and deduplicating logs from initial_log_queue_state. The result is pushed to final_queue_state.

With sorting, we get 2 queues – a simple one, and a sorted one.

We start with the witness allocation:

let mut structured_input =
        EventsDeduplicatorInputOutput::alloc_ignoring_outputs(cs, closed_form_input.clone());

Now the scheme is familiar.

Check if we didn't take elements from the queue:

unsorted_queue_from_passthrough_state.enforce_trivial_head(cs);

Judging by the flag, we choose a queue:

let state = QueueState::conditionally_select(
        cs,
        structured_input.start_flag,
        &unsorted_queue_from_passthrough_state,
        &unsorted_queue_from_fsm_input_state,
    );

Wrap the state and witnesses for it in StorageLogQueue, thereby preparing the input data for inner:

let mut unsorted_queue = StorageLogQueue::<F, R>::from_state(cs, state);

  use std::sync::Arc;
  let initial_queue_witness = CircuitQueueWitness::from_inner_witness(initial_queue_witness);
  unsorted_queue.witness = Arc::new(initial_queue_witness);

  let intermediate_sorted_queue_from_passthrough_state = structured_input
      .observable_input
      .intermediate_sorted_queue_state;

For sorted_queue, it is the same procedure.

let challenges = crate::utils::produce_fs_challenges::<
        F,
        CS,
        R,
        QUEUE_STATE_WIDTH,
        { MEMORY_QUERY_PACKED_WIDTH + 1 },
        DEFAULT_NUM_PERMUTATION_ARGUMENT_REPETITIONS,
    >(
        cs,
        structured_input
            .observable_input
            .initial_log_queue_state
            .tail,
        structured_input
            .observable_input
            .intermediate_sorted_queue_state
            .tail,
        round_function,
    );

Again, if it is not the rest cycle (start_flag == false), we should choose fsm:

let one = Num::allocated_constant(cs, F::ONE);
let initial_lhs = Num::parallel_select(
    cs,
    structured_input.start_flag,
    &[one; DEFAULT_NUM_PERMUTATION_ARGUMENT_REPETITIONS],
    &structured_input.hidden_fsm_input.lhs_accumulator,
);

let initial_rhs = Num::parallel_select(
    cs,
    structured_input.start_flag,
    &[one; DEFAULT_NUM_PERMUTATION_ARGUMENT_REPETITIONS],
    &structured_input.hidden_fsm_input.rhs_accumulator,
);

Depending on the flag, we prepare all the information for inner part:

let zero_u32 = UInt32::zero(cs);
let previous_key = UInt32::conditionally_select(
    cs,
    structured_input.start_flag,
    &zero_u32,
    &structured_input.hidden_fsm_input.previous_key,
);
let empty_storage = LogQuery::placeholder(cs);
let previous_item = LogQuery::conditionally_select(
    cs,
    structured_input.start_flag,
    &empty_storage,
    &structured_input.hidden_fsm_input.previous_item,
);

After inner part we check unsorted_queue and intermediate_sorted_queue.:

let unsorted_is_empty = unsorted_queue.is_empty(cs);
let sorted_is_empty = intermediate_sorted_queue.is_empty(cs);

Boolean::enforce_equal(cs, &unsorted_is_empty, &sorted_is_empty);

We check that permutation accumulators are equal and if the queues are already empty:

let completed = unsorted_queue.length.is_zero(cs);
    for (lhs, rhs) in new_lhs.iter().zip(new_rhs.iter()) {
        Num::conditionally_enforce_equal(cs, completed, lhs, rhs);
    }

Finally, we compute a commitment to PublicInput and allocate it as witness variables.

let input_commitment = commit_variable_length_encodable_item(cs, &compact_form, round_function);
for el in input_commitment.iter() {
    let gate = PublicInputGate::new(el.get_variable());
    gate.add_to_cs(cs);
}

Inner part

Note: we have specific logic for rollback. When we have an event of some function and then that function makes a return then we should cancel this event. Inside the VM, we create exactly the same event: same key, block number, timestamp, etc. the only change is that the rollback flag is now true. In the inner part, first sort and look for these pairs and self-destruct them.

There are two cases: when unsorted_queue is empty, but it's the only circuit, in this case. Otherwise, we continue, and then it's not trivial.

let no_work = unsorted_queue.is_empty(cs);
let mut previous_is_trivial = Boolean::multi_or(cs, &[no_work, is_start]);

Additional checks for length. We should always check whether the sorted queue and the normal queue are of the same length.

let unsorted_queue_length = Num::from_variable(unsorted_queue.length.get_variable());
let intermediate_sorted_queue_length =
    Num::from_variable(intermediate_sorted_queue.length.get_variable());

Num::enforce_equal(
    cs,
    &unsorted_queue_length,
    &intermediate_sorted_queue_length,
);

We can pop elements if unsorted_queue is empty. That’s why every time we set up the flags original_is_empty, sorted_is_empty. We also ensure that items are "write" unless it's a padding.

let original_is_empty = unsorted_queue.is_empty(cs);
let sorted_is_empty = intermediate_sorted_queue.is_empty(cs);
Boolean::enforce_equal(cs, &original_is_empty, &sorted_is_empty);

let should_pop = original_is_empty.negated(cs);
let is_trivial = original_is_empty;

let (_, original_encoding) = unsorted_queue.pop_front(cs, should_pop);
let (sorted_item, sorted_encoding) = intermediate_sorted_queue.pop_front(cs, should_pop);

Check if keys are equal and check a value. We compare timestamps and then resolve logic over rollbacks, so the only way when keys are equal can be when we do a rollback. Ensure sorting for uniqueness timestamp and rollback flag. We know that timestamps are unique across logs, and are also the same between write and rollback. Keys are always ordered no matter what, and are never equal unless it's padding:

let sorting_key = sorted_item.timestamp;
let (keys_are_equal, new_key_is_smaller) =
                unpacked_long_comparison(cs, &[previous_key], &[sorting_key]);
new_key_is_smaller.conditionally_enforce_false(cs, should_pop);

There are only two cases when keys are equal:

  • it's a padding element

  • it's a rollback

It's enough to compare timestamps, as the VM circuit guarantees uniqueness if it's not a padding. Now ensure sorting:

let previous_is_not_rollback = previous_item.rollback.negated(cs);
let enforce_sequential_rollback = Boolean::multi_and(
    cs,
    &[previous_is_not_rollback, sorted_item.rollback, should_pop],
);
keys_are_equal.conditionally_enforce_true(cs, enforce_sequential_rollback);

let same_log = UInt32::equals(cs, &sorted_item.timestamp, &previous_item.timestamp);

let values_are_equal =
    UInt256::equals(cs, &sorted_item.written_value, &previous_item.written_value);

let negate_previous_is_trivial = previous_is_trivial.negated(cs);
let should_enforce = Boolean::multi_and(cs, &[same_log, negate_previous_is_trivial]);

values_are_equal.conditionally_enforce_true(cs, should_enforce);

let this_item_is_non_trivial_rollback =
    Boolean::multi_and(cs, &[sorted_item.rollback, should_pop]);
let negate_previous_item_rollback = previous_item.rollback.negated(cs);
let previous_item_is_non_trivial_write = Boolean::multi_and(
    cs,
    &[negate_previous_item_rollback, negate_previous_is_trivial],
);
let is_sequential_rollback = Boolean::multi_and(
    cs,
    &[
        this_item_is_non_trivial_rollback,
        previous_item_is_non_trivial_write,
    ],
);
same_log.conditionally_enforce_true(cs, is_sequential_rollback);

Decide if we should add the previous into the queue. We add only if the previous one is not trivial, it had a different key, and it wasn't rolled back:

let negate_same_log = same_log.and(cs, should_pop).negated(cs);
let add_to_the_queue = Boolean::multi_and(
    cs,
    &[
        negate_previous_is_trivial,
        negate_same_log,
        negate_previous_item_rollback,
    ],
);

Further, we don't need in our LogQueue some fields, so we just clean up:

let boolean_false = Boolean::allocated_constant(cs, false);
let query_to_add = LogQuery {
    address: previous_item.address,
    key: previous_item.key,
    read_value: UInt256::zero(cs),
    written_value: previous_item.written_value,
    rw_flag: boolean_false,
    aux_byte: UInt8::zero(cs),
    rollback: boolean_false,
    is_service: previous_item.is_service,
    shard_id: previous_item.shard_id,
    tx_number_in_block: previous_item.tx_number_in_block,
    timestamp: UInt32::zero(cs),
};

Finalization step - same way, check if the last item is not a rollback:

let now_empty = unsorted_queue.is_empty(cs);

let negate_previous_is_trivial = previous_is_trivial.negated(cs);
let negate_previous_item_rollback = previous_item.rollback.negated(cs);
let add_to_the_queue = Boolean::multi_and(
    cs,
    &[
        negate_previous_is_trivial,
        negate_previous_item_rollback,
        now_empty,
    ],
);
let boolean_false = Boolean::allocated_constant(cs, false);
let query_to_add = LogQuery {
    address: previous_item.address,
    key: previous_item.key,
    read_value: UInt256::zero(cs),
    written_value: previous_item.written_value,
    rw_flag: boolean_false,
    aux_byte: UInt8::zero(cs),
    rollback: boolean_false,
    is_service: previous_item.is_service,
    shard_id: previous_item.shard_id,
    tx_number_in_block: previous_item.tx_number_in_block,
    timestamp: UInt32::zero(cs),
};

result_queue.push(cs, query_to_add, add_to_the_queue);

unsorted_queue.enforce_consistency(cs);
intermediate_sorted_queue.enforce_consistency(cs);

We generate challenges and accumulators for the permutation argument. A detailed explanation can be found .

The next block of code is sorting. You can find the main idea .

GitHub
GitHub
GitHub
here
here