halo2_base/virtual_region/
lookups.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, OnceLock};

use getset::{CopyGetters, Getters, Setters};

use crate::ff::Field;
use crate::halo2_proofs::{
    circuit::{Region, Value},
    plonk::{Advice, Column},
};
use crate::utils::halo2::{constrain_virtual_equals_external, raw_assign_advice};
use crate::{AssignedValue, ContextTag};

use super::copy_constraints::SharedCopyConstraintManager;
use super::manager::VirtualRegionManager;

/// Basic dynamic lookup table gadget.
pub mod basic;

/// A manager that can be used for any lookup argument. This manager automates
/// the process of copying cells to designed advice columns with lookup enabled.
/// It also manages how many such advice columns are necessary.
///
/// ## Detailed explanation
/// If we have a lookup argument that uses `ADVICE_COLS` advice columns and `TABLE_COLS` table columns, where
/// the table is either fixed or dynamic (advice), then we want to dynamically allocate chunks of `ADVICE_COLS` columns
/// that have the lookup into the table **always on** so that:
/// - every time we want to lookup [_; ADVICE_COLS] values, we copy them over to a row in the special
/// lookup-enabled advice columns.
/// - note that just for assignment, we don't need to know anything about the table itself.
/// Note: the manager does not need to know the value of `TABLE_COLS`.
///
/// We want this manager to be CPU thread safe, while ensuring that the resulting circuit is
/// deterministic -- the order in which the cells to lookup are added matters.
/// The current solution is to tag the cells to lookup with the context id from the [`Context`](crate::Context) in which
/// it was called, and add virtual cells sequentially to buckets labelled by id.
/// The virtual cells will be assigned to physical cells sequentially by id.
/// We use a `BTreeMap` for the buckets instead of sorting to cells, to ensure that the order of the cells
/// within a bucket is deterministic.
/// The assumption is that the [`Context`](crate::Context) is thread-local.
///
/// Cheap to clone across threads because everything is in [Arc].
#[derive(Clone, Debug, Getters, CopyGetters, Setters)]
pub struct LookupAnyManager<F: Field + Ord, const ADVICE_COLS: usize> {
    /// Shared cells to lookup, tagged by (type id, context id).
    #[allow(clippy::type_complexity)]
    pub cells_to_lookup: Arc<Mutex<BTreeMap<ContextTag, Vec<[AssignedValue<F>; ADVICE_COLS]>>>>,
    /// Global shared copy manager
    #[getset(get = "pub", set = "pub")]
    copy_manager: SharedCopyConstraintManager<F>,
    /// Specify whether constraints should be imposed for additional safety.
    #[getset(get_copy = "pub")]
    witness_gen_only: bool,
    /// Flag for whether `assign_raw` has been called, for safety only.
    pub(crate) assigned: Arc<OnceLock<()>>,
}

impl<F: Field + Ord, const ADVICE_COLS: usize> LookupAnyManager<F, ADVICE_COLS> {
    /// Creates a new [LookupAnyManager] with a given copy manager.
    pub fn new(witness_gen_only: bool, copy_manager: SharedCopyConstraintManager<F>) -> Self {
        Self {
            witness_gen_only,
            cells_to_lookup: Default::default(),
            copy_manager,
            assigned: Default::default(),
        }
    }

    /// Add a lookup argument to the manager.
    pub fn add_lookup(&self, tag: ContextTag, cells: [AssignedValue<F>; ADVICE_COLS]) {
        self.cells_to_lookup
            .lock()
            .unwrap()
            .entry(tag)
            .and_modify(|thread| thread.push(cells))
            .or_insert(vec![cells]);
    }

    /// The total number of virtual rows needed to special lookups
    pub fn total_rows(&self) -> usize {
        self.cells_to_lookup.lock().unwrap().iter().flat_map(|(_, advices)| advices).count()
    }

    /// The optimal number of `ADVICE_COLS` chunks of advice columns with lookup enabled for this
    /// particular lookup argument that we should allocate.
    pub fn num_advice_chunks(&self, usable_rows: usize) -> usize {
        let total = self.total_rows();
        (total + usable_rows - 1) / usable_rows
    }

    /// Clears state
    pub fn clear(&mut self) {
        self.cells_to_lookup.lock().unwrap().clear();
        self.copy_manager.lock().unwrap().clear();
        self.assigned = Arc::new(OnceLock::new());
    }

    /// Deep clone with the specified copy manager. Unsets `assigned`.
    pub fn deep_clone(&self, copy_manager: SharedCopyConstraintManager<F>) -> Self {
        Self {
            witness_gen_only: self.witness_gen_only,
            cells_to_lookup: Arc::new(Mutex::new(self.cells_to_lookup.lock().unwrap().clone())),
            copy_manager,
            assigned: Default::default(),
        }
    }
}

impl<F: Field + Ord, const ADVICE_COLS: usize> Drop for LookupAnyManager<F, ADVICE_COLS> {
    /// Sanity checks whether the manager has assigned cells to lookup,
    /// to prevent user error.
    fn drop(&mut self) {
        if Arc::strong_count(&self.cells_to_lookup) > 1 {
            return;
        }
        if self.total_rows() > 0 && self.assigned.get().is_none() {
            dbg!("WARNING: LookupAnyManager was not assigned!");
        }
    }
}

impl<F: Field + Ord, const ADVICE_COLS: usize> VirtualRegionManager<F>
    for LookupAnyManager<F, ADVICE_COLS>
{
    type Config = Vec<[Column<Advice>; ADVICE_COLS]>;

    fn assign_raw(&self, config: &Self::Config, region: &mut Region<F>) {
        let mut copy_manager =
            (!self.witness_gen_only).then(|| self.copy_manager().lock().unwrap());
        let cells_to_lookup = self.cells_to_lookup.lock().unwrap();
        // Copy the cells to the config columns, going left to right, then top to bottom.
        // Will panic if out of rows
        let mut lookup_offset = 0;
        let mut lookup_col = 0;
        for advices in cells_to_lookup.iter().flat_map(|(_, advices)| advices) {
            if lookup_col >= config.len() {
                lookup_col = 0;
                lookup_offset += 1;
            }
            for (advice, &column) in advices.iter().zip(config[lookup_col].iter()) {
                let bcell =
                    raw_assign_advice(region, column, lookup_offset, Value::known(advice.value));
                if let Some(copy_manager) = copy_manager.as_mut() {
                    constrain_virtual_equals_external(region, *advice, bcell.cell(), copy_manager);
                }
            }

            lookup_col += 1;
        }
        // We cannot clear `cells_to_lookup` because keygen_vk and keygen_pk both call this function
        let _ = self.assigned.set(());
    }
}