github.com/consensys/gnark@v0.11.0/constraint/blueprint_logderivlookup.go (about)

     1  package constraint
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  )
     7  
     8  // TODO @gbotrel this shouldn't be there, but we need to figure out a clean way to serialize
     9  // blueprints
    10  
    11  // BlueprintLookupHint is a blueprint that facilitates the lookup of values in a table.
    12  // It is essentially a hint to the solver, but enables storing the table entries only once.
    13  type BlueprintLookupHint struct {
    14  	EntriesCalldata []uint32
    15  
    16  	// stores the maxLevel of the entries computed by WireWalker
    17  	maxLevel         Level
    18  	maxLevelPosition int
    19  	maxLevelOffset   int
    20  
    21  	// cache the resolved entries by the solver
    22  	cachedEntries []Element
    23  	cachedOffset  int
    24  	lock          sync.Mutex
    25  }
    26  
    27  // ensures BlueprintLookupHint implements the BlueprintStateful interface
    28  var _ BlueprintStateful = (*BlueprintLookupHint)(nil)
    29  
    30  func (b *BlueprintLookupHint) Solve(s Solver, inst Instruction) error {
    31  	nbEntries := int(inst.Calldata[1])
    32  
    33  	// check if we already cached the entries
    34  	b.lock.Lock()
    35  	if len(b.cachedEntries) < nbEntries {
    36  		// we need to cache more entries
    37  		offset, delta := b.cachedOffset, 0
    38  		for i := len(b.cachedEntries); i < nbEntries; i++ {
    39  			b.cachedEntries = append(b.cachedEntries, Element{})
    40  			b.cachedEntries[i], delta = s.Read(b.EntriesCalldata[offset:])
    41  			offset += delta
    42  		}
    43  		b.cachedOffset = offset
    44  	}
    45  	b.lock.Unlock()
    46  
    47  	// we only append to the entries and never resize the slice; so we can access these indices safely
    48  	entries := b.cachedEntries[:nbEntries]
    49  
    50  	nbInputs := int(inst.Calldata[2])
    51  
    52  	// read the inputs from the instruction
    53  	inputs := make([]Element, nbInputs)
    54  	offset, delta := 3, 0
    55  	for i := 0; i < nbInputs; i++ {
    56  		inputs[i], delta = s.Read(inst.Calldata[offset:])
    57  		offset += delta
    58  	}
    59  
    60  	// set the outputs
    61  	nbOutputs := nbInputs
    62  
    63  	for i := 0; i < nbOutputs; i++ {
    64  		idx, isUint64 := s.Uint64(inputs[i])
    65  		if !isUint64 || idx >= uint64(len(entries)) {
    66  			return fmt.Errorf("lookup query too large")
    67  		}
    68  		// we set the output wire to the value of the entry
    69  		s.SetValue(uint32(i+int(inst.WireOffset)), entries[idx])
    70  	}
    71  	return nil
    72  }
    73  
    74  func (b *BlueprintLookupHint) Reset() {
    75  	// first we need to compute the capacity; that is 1 element per linear expression in the entries.
    76  	// this must be accurate since solver is multi threaded and we don't want to resize the slice
    77  	// while the solver is running.
    78  	capacity := 0
    79  	for i := 0; i < len(b.EntriesCalldata); i++ {
    80  		n := int(b.EntriesCalldata[i]) // length of the linear expression
    81  		capacity++
    82  		i += 2 * n // skip the linear expression
    83  	}
    84  
    85  	b.cachedEntries = make([]Element, 0, capacity)
    86  	b.cachedOffset = 0
    87  }
    88  
    89  func (b *BlueprintLookupHint) CalldataSize() int {
    90  	// variable size
    91  	return -1
    92  }
    93  func (b *BlueprintLookupHint) NbConstraints() int {
    94  	return 0
    95  }
    96  
    97  // NbOutputs return the number of output wires this blueprint creates.
    98  func (b *BlueprintLookupHint) NbOutputs(inst Instruction) int {
    99  	return int(inst.Calldata[2])
   100  }
   101  
   102  func (b *BlueprintLookupHint) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level {
   103  	// depend on the table UP to the number of entries at time of instruction creation.
   104  	nbEntries := int(inst.Calldata[1])
   105  
   106  	// check if we already cached the max level
   107  	if b.maxLevelPosition-1 < nbEntries { // adjust for default value of b.maxLevelPosition (0)
   108  
   109  		j := b.maxLevelOffset // skip the entries we already processed
   110  		for i := b.maxLevelPosition; i < nbEntries; i++ {
   111  			// first we have the length of the linear expression
   112  			n := int(b.EntriesCalldata[j])
   113  			j++
   114  			for k := 0; k < n; k++ {
   115  				wireID := b.EntriesCalldata[j+1]
   116  				j += 2
   117  				if !tree.HasWire(wireID) {
   118  					continue
   119  				}
   120  				if level := tree.GetWireLevel(wireID); (level + 1) > b.maxLevel {
   121  					b.maxLevel = level + 1
   122  				}
   123  			}
   124  		}
   125  		b.maxLevelOffset = j
   126  		b.maxLevelPosition = nbEntries
   127  	}
   128  
   129  	maxLevel := b.maxLevel - 1 // offset for default value.
   130  
   131  	// update the max level with the lookup query inputs wires
   132  	nbInputs := int(inst.Calldata[2])
   133  	j := 3
   134  	for i := 0; i < nbInputs; i++ {
   135  		// first we have the length of the linear expression
   136  		n := int(inst.Calldata[j])
   137  		j++
   138  		for k := 0; k < n; k++ {
   139  			wireID := inst.Calldata[j+1]
   140  			j += 2
   141  			if !tree.HasWire(wireID) {
   142  				continue
   143  			}
   144  			if level := tree.GetWireLevel(wireID); level > maxLevel {
   145  				maxLevel = level
   146  			}
   147  		}
   148  	}
   149  
   150  	// finally we have the outputs
   151  	maxLevel++
   152  	for i := 0; i < nbInputs; i++ {
   153  		tree.InsertWire(uint32(i+int(inst.WireOffset)), maxLevel)
   154  	}
   155  
   156  	return maxLevel
   157  }