diff --git a/spec/chapters/field.typ b/spec/chapters/field.typ new file mode 100644 index 000000000..1333ed77b --- /dev/null +++ b/spec/chapters/field.typ @@ -0,0 +1 @@ +TODO diff --git a/spec/chapters/field_decode.typ b/spec/chapters/field_decode.typ new file mode 100644 index 000000000..6921ceac3 --- /dev/null +++ b/spec/chapters/field_decode.typ @@ -0,0 +1,2 @@ + +TODO diff --git a/spec/chapters/recursion.typ b/spec/chapters/recursion.typ new file mode 100644 index 000000000..481d6733a --- /dev/null +++ b/spec/chapters/recursion.typ @@ -0,0 +1,392 @@ +#import "/meta.typ": et, aside + +// Spaces and instances +#let (programSpace, program) = ($cal(F)$, $f$) +#let (inputSpace, input) = ($II$, $bb(i)$) +#let (instanceSpace, instance, instance2) = ($XX$, $bb(x)$, $bb(y)$) +#let (witnessSpace, witness) = ($WW$, $bb(w)$) +#let (proofSpace, proof) = ($bb(Pi)$, $pi$) + +#let (commitmentSpace, commitment) = ($cal(C)$, $bb(c)$) +#let commit(x) = $overline(#x)$ +#let comm(x) = $commit(#x)$ + +#let hash = $H$ + +#let relation = $cal(R)$ +#let language = $cal(L)$ + +#let verifierSpace = $cal(V)$ +#let (prove, verify) = ($italic("p")$, $italic("v")$) + +// Mathematical symbols +#let (zero, one) = ($0$, $1$) +#let iff = $arrow.double.l.r$ +#let implies = $arrow.double.r$ +#let prob = $PP$ +#let to = math.arrow.r + +#show math.equation.where(block: false): box + +#set list(marker: [---]) + += Proof recursion +Let $BB := { zero, one }$ denote the boolean set and let +$programSpace := {program: inputSpace times witnessSpace to BB}$ denote +the set of functions mapping the (public) input space $inputSpace$ and (private) +witness space $witnessSpace$ to this set. +We define instance space $instanceSpace := programSpace times inputSpace tilde.equiv {program: witnessSpace to BB}$; +program-input pairs $(program, input) in instanceSpace$ +are henceforth referred to as _function instances_, or simply _instances_. +Where the individual components of the pair are irrelevant, an instance is +denoted as $instance in instanceSpace$. + +We define relation $relation subset.eq instanceSpace times witnessSpace$ where $((program, input), witness) in relation$ if $program(input, witness) = 1$. +This relation induces the language $language subset.eq instanceSpace$ of _solvable instances_, +where $instance in language$ if there exists a witness $witness$ for which $(instance, witness)in relation$. + +Lastly, we introduce the instance commitment function $c: instanceSpace to commitmentSpace$. +Note that this commitment scheme does not involve randomness; it is a _determistic_ scheme. +Randomness is typically required to make a commitment _hiding_. +For the purposes of this discussion, we are not concerned with this property, +as the function will only be used for committing to public information. +To simplify notation, we henceforth use $commit(instance)$ to represent the commitment $c(instance)$ of $instance$. + +We now assume the existence of _proof system_ $(prove, verify)$ with +prover $prove: instanceSpace times witnessSpace to proofSpace$ and +verifier $verify: commitmentSpace times proofSpace to BB$ that is both +_complete_ --- i.e., $verify$ accepts all valid proofs generated by $prove$ --- +and _sound_ --- i.e., one cannot create an acceptable proof for an unsolvable instance. + +Translating this to the purposes of this VM, a prover wishes to convince the verifier +that for some agreed upon program ($program in programSpace$) and specified public input ($input in inputSpace$), +they know a private input ($witness in witnessSpace$) such that the program terminates successfully +(i.e., $(program, input) in language$). +To this end, the prover uses $prove(program(input; dot); witness)$ +to construct some proof $proof in proofSpace$ and sends this to the verifier. +They then use $verify(comm(program(input; dot)), proof)$ to check that the proof is valid, +convincing them of the prover's claim. + +== Recursively proving proof verification +Now observe that the verifier $verify$ is itself a function in +$verifierSpace := {hat(f): commitmentSpace times proofSpace to BB} subset.eq programSpace$. +This means that we can use $prove$ to prove that the verification of a proof $proof$ +for a given instance $instance$ succeeds: +$ + &prove(verify(comm(instance), dot); proof) = proof', text("and") + &verify(comm(verify(comm(instance), dot)), proof') = one. +$ +This new proof $proof'$ thus attests to _the existence of a proof $proof$ that +satisfies the verifier on the given instance $instance$_. + +This concept, colloquially known as _proof recursion_, can be applied repeatedly. +The technique is specifically beneficial for _succinct_ proving systems where proof size +typically shrinks (and verification time reduces) as the level of recursion increases. +The technique is mostly useful in settings where the extra time spent by the prover +is outweighed by the time saved by the verifier(s), +e.g., a computationally constrained verifier, or multiple verifiers. + +== Resolving growing instance complexity +While recursive proving leads to a decrease in proof size, this is naively traded off +against an increase in instance complexity. +Looking at a depth-two recursive proof, +$ + prove(verify(comm(verify(comm(instance), dot)), dot); proof') = proof'', text("and") + verify(comm(verify(comm(verify(comm(instance), dot)), dot)), proof'') = one +$ +we see that the verifier first has to derive the commitment +$comm(verify(comm(verify(comm(instance), dot)), dot))$ +from the given base instance $instance$ before verifying the proof. +This increase in verifier computation is undesirable and should be avoided. + +A solution to this, is to leverage the following variation to the verification algorithm: +$ + verify': commitmentSpace^2 times proofSpace: ([comm(x), comm(y)], proof) mapsto + verify(comm(x), proof) or verify(comm(y([comm(x), comm(y)], dot)), proof) +$ +where it is assumed that $comm(y([comm(x), comm(y)], dot)) in commitmentSpace$ can be constructed efficiently from $comm(x), comm(y) in commitmentSpace$. +Now observe that setting $(comm(x), comm(y)) = (commit(instance), commit(verify'))$ yields +$ + verify'([comm(instance), comm(verify')], proof) + = verify(comm(instance), proof) or verify(comm(verify'([comm(instance), comm(verify')], dot)), proof). +$ +In words, $verify'([comm(instance), comm(verify')], dot)$ accepts only if $proof$ is either +a) proof of $instance in language$, or +b) proof of _the existence of a (different) proof_ that satisfies $verify'([comm(instance), comm(verify')], dot)$. +We can now recursively expand the expression, yielding +$ + verify'(commit(instance), commit(verify'), proof) + &= verify(comm(instance), proof) or verify(comm(verify'([comm(instance), comm(verify')], dot)), proof)\ + &= verify(comm(instance), proof) or verify(comm(verify(comm(instance), dot) or verify(comm(verify'([comm(instance), comm(verify')]; dot)), dot)), proof)\ + &= verify(comm(instance), proof) or verify(comm(verify(comm(instance), dot) or verify(comm(verify(comm(instance), dot) or verify(comm(verify'([comm(instance), comm(verify')], dot)), dot)), dot)), proof)\ + &= verify(comm(instance), proof) or verify(comm(verify(comm(instance), dot) or verify(comm(verify(comm(instance), dot) or verify(comm(verify(comm(instance), dot) or verify(comm(verify'([comm(instance), comm(verify')], dot)), dot)), dot)), dot)), proof),\ + &&text(italic("etc.")) +$ +This shows us that a prover can use $verify'$ to shrink a proof $proof$ attesting +to $instance in language$ recursively any number of times, such that the ultimate +verification will still pass; verification is _complete_. +Secondly, note that the size of the verification input +$(comm(instance), comm(verify'))$ is _constant_ in the depth of the recursion. +In fact, the size of the verification instance is typically fully determined by +$comm(instance)$, since $comm(verify')$ can often be precomputed. + +It is important to note that we have not proven soundness of this construction. +For example, there may exist proofs that _attest to the existence of itself_ in a finite number of recursion steps. +Such a proof would be accepted by $verify'$ even if $instance in.not language$. +In practice, one might be able to prevent this problem by including a recursion-level counter in the proof. +The existence of other soundness gaps are not ruled out by the authors. + +#aside([$comm(verify')$ absorption])[ + Note that $commit(verify')$ must be provided to $verify'$ as a _parameter_; + absorbing it into $verify'$ would imply an object containing a cryptographic + commitment of itself at a static location, which is practically impossible. +] + += Split verification +#let (record, recordSpace) = ($bb(r)$, $Re$) + +Proof recursion involves executing the verification algorithm _inside_ the VM. +In practice, we find that the set of operations utilized for verification differs vastly from +those typically performed by guest programs. +Specifically, verification primarily involves hashing and (extension) field arithmetic, +where especially the second is absent in typical guest programs. + +Emulating field arithmetic on a binary arithmetic-oriented VM typically +incurs significant computational overhead. +With the aim of avoiding a performance penalty on this front, we show how one +might split the verification process in two halves, +such that either half can be executed on a VM with an instruction set tailored to its needs, +while maintaining completeness and soundness of the verification. + +== Communication +#let product(f, g) = $(#f and #g)$ + +When executing the two verification-algorithm halves on distinct VMs, +no direct communication between both algorithms is possible, +even if both VMs are proven using the same proof system. +Yet, practice shows that effective algorithm splits requires some form of communication +between both halves: typically one half performs a verification step to a certain +point, after which the other half continues verification starting from this +intermediate state. +Hence, support for communication is desirable. + +Briefly zooming out, we observe that for two algorithm halves to jointly +verify a proof, it is paramount that both subprograms are provided identical +input. +Moreover, we note that split verification is used in _recursion_ only: +the verifier verifies the ultimate proof _outside_ the VM, where the monolithic +verification algorithm can be executed efficiently. +Furthermore, all input provided during recursion is hinted by the prover, which +has access to the full execution trace before proving, including any information +that would ideally be sent between the two verification halves. + +To achieve communication between two programs running on different VMs, +we thus introduce the concept of a prover-hinted _communication record_ $record$ +provided as input to both subprocesses. +All values that are to be communicated from one part to another, are stated on this record. +For each value on the record, the "sending" half _verifies_ that it is as expected, +whilst the "receiving" half _assumes_ its correctness and resumes verification under this assumption. +One can now conclude that the proof satisfies the instance +when both algorithm-halves accept the same record for this input. + +#aside("Coupling")[ + As observed, both verification halves must be synchronized to correctly verify a proof. + This implies that some coupling between both halves must exist. + This design utilizes little coupling in the VM design, instead forcing + the guest programs to solve synchronization, as a result introducing the coupling there. + + This no-coupling VM design permits one of the two halves to transition to a + different proof system (e.g., moving to Flock + #footnote(link( + "https://eprint.iacr.org/2026/1329", + "Flock: Fast Proving for Batch Boolean Computations. src: https://eprint.iacr.org/2026/1329" + )) + to accelerate hash-verification) while incurring as little design overhead as possible. +] + +More formally, we define +$verify_0, verify_1: commitmentSpace times proofSpace times recordSpace to BB$ as a valid _split_ of +verifier $verify in verifierSpace$ if +$ + forall (comm(instance), proof) in commitmentSpace times proofSpace: verify(comm(instance), proof) = 1 iff exists record in recordSpace text("s.t.") verify_0(comm(instance), [proof, record]) and verify_1(comm(instance), [proof, record]) = 1, +$ +where $recordSpace$ denotes the _communication record space_. +One can now express +$ + verify(comm(instance), proof) + = verify_0(comm(instance), [proof, d_verify (comm(instance), proof)]) + dot verify_1(comm(instance), [proof, d_verify (comm(instance), proof)]). +$ +where $d_verify (comm(instance), proof)$ denotes the record deriviation function for $verify$. +We introduce the function product $product(f, g)(input, witness) := f(input, witness) dot g(input, witness)$ for $f, g in programSpace$, which allows us to express +$ + prove(verify(comm(instance), dot); proof) + &= prove(product(verify_0, verify_1)(comm(instance), dot); [proof, record]) + &= proof', +$ +with $record := d_verify (comm(instance), proof)$. + +Summarizing, we have now expressed recursive verification in terms of a +split verifier, where the same instance-proof-record triple was provided to +both verification halves. +The produced proof $proof'$ now attests to $instance in language$ when +$verify(comm(product(verify_0, verify_1)(comm(instance), dot)), proof') = 1$. + += Split proving +#et("todo") + += Split-recursion +With proof recursion and split verification formalized, we now combine both concepts into a unified system. +To this end, we first introduce the selector function +$ + &Delta_cal(A): cal(A)^2 times BB to cal(A): (x, y, b) mapsto cases(x & text("if") b=0, y & text("if") b=1), +$ +which, given a bit $b in BB$ and two elements $x,y$ of some space $cal(A)$, selects +an element based on the value of $b$. +We can now use this selector to define $verify^*$ --- a variation to $verify'$ --- as follows: +$ + verify^*([comm(x), comm(y)], [proof, b]) + :&= Delta_BB (verify(comm(x), proof), verify(comm(y([comm(x), comm(y)], dot)), proof), b)\ + &= verify(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), b), proof). +$ +Note here that one can reconstruct $verify'$ from $verify^*$ as +$ + sum_(b in BB) verify^*([comm(x), comm(y)], [proof, b]) + &=^((triangle)) or.big_(b in BB) verify^*([comm(x), comm(y)], [proof, b])\ + &= verify(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), 0), proof) or verify(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), 1), proof)\ + &= verify(comm(x), proof) or verify(comm(y([comm(x), comm(y)], dot)), proof)\ + &= verify'(comm(x), comm(y), proof),\ +$ +where $(triangle)$ holds under the assumption that a single proof $proof$ cannot +attest to two distinct instances. +This makes the `OR` operation ($or$) effectively equivalent to the `XOR` operation, +which is equivalent to addition in $BB$. +Hence, $verify^*$ and $verify'$ are effectively equivalent, except that $verify^*$ +has the proof include a bit $b$ indicating whether it is verifying a base proof or recursion proof. + +We now observe that +$ + verify^*_0([comm(x), comm(y)], [[proof, b], r]) + &:= verify_0(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), b), [proof, r]),\ + verify^*_1([comm(x), comm(y)], [[proof, b], r]) + &:= verify_1(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), b), [proof, r]) +$ +jointly form a valid split of $verify^*$, since +$ + verify^*([comm(x), comm(y)], [proof, b]) + &= verify(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), b), proof)\ + &= product(verify_0, verify_1)(Delta_C (comm(x), comm(y([comm(x), comm(y)], dot)), b), [proof, r])\ + &= product(verify^*_0, verify^*_1)([comm(x), comm(y)], [[proof, b], r]),\ +$ +when +$r = d_(verify^*) ([comm(x), comm(y([comm(x), comm(y)], dot))], [proof, b])$. +By selecting $(comm(x), comm(y)) = (comm(instance), comm(product(verify^*_0, verify^*_1)))$, +we now obtain +$ + product(verify^*_0, verify^*_1)([comm(instance), comm(product(verify^*_0, verify^*_1))], [[proof, b], r]) + &= verify^*([comm(instance), comm(product(verify^*_0, verify^*_1))], [proof, b])\ + &= verify(Delta_commitmentSpace (comm(instance), comm(product(verify^*_0, verify^*_1)([comm(instance),comm(product(verify^*_0, verify^*_1))], dot)), b), proof)\ + &= Delta_BB (verify(comm(instance), proof), verify(comm(product(verify^*_0, verify^*_1)([comm(instance),comm(product(verify^*_0, verify^*_1))], dot)), proof), b)\ + &= verify(comm(instance), proof) dot (1-b) + verify(comm(product(verify^*_0, verify^*_1)([comm(instance),comm(product(verify^*_0, verify^*_1))], dot)), proof) dot b, +$ +i.e., a split verification algorithm that checks whether $proof$ attests a) to $instance in language$ when $b=0$ or b) to the existence of a proof that does when $b=1$. +Importantly, this can be achieved recursively, as +$ + #h(8em) // alignment purposes + &prove(instance; witness) &&to proof_(0),\ + &prove(product(verify^*_0, verify^*_1)([comm(instance), comm(product(verify^*_0, verify^*_1))], dot); [[proof_(0), 0], r_(0)]) &&to [proof_(1), 1],\ + &prove(product(verify^*_0, verify^*_1)([comm(instance), comm(product(verify^*_0, verify^*_1))], dot); [[proof_(1), 1], r_(1)]) &&to [proof_(2), 1],\ + &prove(product(verify^*_0, verify^*_1)([comm(instance), comm(product(verify^*_0, verify^*_1))], dot); [[proof_(2), 1], r_(2)]) &&to [proof_(3), 1], + &&#h(8em)text(italic("etc.")) +$ +with +$ + r_i :&= d_(verify^*)(comm(product(verify^*_0, verify^*_1)([comm(instance), comm(product(verify^*_0, verify^*_1))], dot)), [proof_i, 1-delta_(i,0)]). +$ + += The theory applied +We now discuss how the split-recursion system is integrated in practice. + +== The split +Let $v$ denote the verification algorithm performing all steps outlined in @verification. +We split $v$ into halves $verify_f$ and $verify_b$ such that an efficient +arithmetization is achieved when the former is executed in the _field-VM_ (@field-VM) +and the latter on the _RiscV-VM_ (@decode through @ecall). +Practically speaking, $verify_f$ is put in charge of all verification steps +involving _field_ arithmetic --- e.g., verifying `FRI` folding, +while $verify_b$ performs all _binary_ arithmetic --- e.g., challenge derivation +by means of the Fiat-Shamir transformation. + +The communication record primarily exists of the various Fiat-Shamir-derived +challenges required by $verify_f$ to complete verification. +During execution, $verify_b$ is in charge of validating these record values, +while $verify_f$ assumes them to be correct. + +#et([update the communication record overview once @verification is complete.]) + +== Recursive instance verification +In @growing-instance-complexity, it is assumed that one can efficiently construct +the commitment $comm(y([comm(x), comm(y)], dot))$ from $(comm(x), comm(y))$. +We show that this assumption holds in practice. + +First, note that for an instance $program(input, dot)$, the program +$program$ and input $input$ are committed to separately. +Recall that a (split) program is encoded as one or more +`DECODE` (@decode) and/or `FIELD-DECODE` (@field-decode) tables, while the +input $input$ --- and private input $witness$ and record $record$ for that matter +--- are made available to the VMs by means of `PAGE` tables (@streaming:chip:page). +In this proof system, the prover commits to each of these tables individually +by means of sharing _the root of their merkle-tree_. +#footnote([ + To clarify the process: + + every table column is _interpolated_, creating a polynomial that + evaluates to the column entries on specific inputs, + + the polynomial is _encoded_, creating a codeword of polynomial evaluations + over a different, larger domain, + + the codewords are _batched_, turning a list of codewords into a codeword of lists, and + + the batched codeword is _merkle-committed_, yielding the tree root. +]) + +In practice, the bytes of the instance and proof are provided in `PAGE` tables in +an order that speeds up processing. +This ordering often puts values to be committed scattered across several tables. +A guest program can condense this representation, by writing the values to be +committed to `stdout`, which must be backed by `PAGE` tables that store the written +bytes in the order they are written. +These `PAGE` tables can then be provided to the verifier, who uses it as part of +verifying the proof. +Note that it is not a problem that comitting in this way is at the discretion of the VM's +guest program: in recursive verification, the guest program is publically known. + +Hence, by having $verify_b ([comm(x), comm(y)], [[proof, b], record])$ write +the commitments $comm(x)$ and $comm(y)$ it is provided to `stdout`, and using +the same commitments to check the proof's `PAGE` tables for the commitment +domain (see @memory) when $b=1$, the ultimate verifier of the +final proof can be confident that the same verification algorithm was used +at every recursion step when verifying +$(comm(x), comm(y)) = (comm(instance), comm(product(verify_b, verify_f)))$. + +== Summary + +*Communication record $record$:*\ +The communication record contains all challenges derived from the proof by +means of the Fiat-Shamir transformation. + +*`RiscV-VM` subalgorithm* +$verify_b ([comm(instance), comm(instance2)], [[proof, b], record])$: +- `write` $comm(instance)$ and $comm(instance2)$ to `stdout`. +- derives challenges from $proof$ by means of Fiat-Shamir, and assert that they + match those located in $record$ at the expected location. +- Performs the binary arithmetic steps required to verify that $proof$ attests + to $instance$ (when $b=0$) or $instance2$ (when $b = 1$), + given the derived challenges. + - Importantly, when $b = 1$, assert that the proof's openings of the `PAGE` + tables backing the commitment domain include $comm(instance)$ and + $comm(instance2)$ at the expected location. + +*`Field-VM` subalgorithm* +$verify_f ([comm(instance), comm(instance2)], [[proof, b], record])$: +- assert that $b in {0, 1}$ +- Performs the field arithmetic steps required to verify that $proof$ attests + to $instance$ (when $b=0$) or $instance2$ (when $b = 1$), + given the challenges stated on the $record$. diff --git a/spec/chapters/verifier.typ b/spec/chapters/verifier.typ new file mode 100644 index 000000000..e85903478 --- /dev/null +++ b/spec/chapters/verifier.typ @@ -0,0 +1,4 @@ +// TODO: +// - sigma protocol, layout the various steps +// - Fiat Shamir transformation into non-interactive protocol. +// diff --git a/spec/meta.typ b/spec/meta.typ index fc6bc783b..eea8f6e4a 100644 --- a/spec/meta.typ +++ b/spec/meta.typ @@ -10,6 +10,7 @@ ("logup", [`LogUp` argument], ), ("memory", [Memory argument], ), ("streaming", [Streaming prover], ), + ("verifier", [Verification], ), )), ("OVERVIEW", ( ("variables", [Variables], ), @@ -52,6 +53,11 @@ ("ecsm", [`ECSM` accelerator], ), ("fext", [Extension field accelerator], ), )), + ("RECURSION", ( + ("recursion", [Recursive verification], ), + ("field", [`Field` VM], ), + ("field_decode", [`Field` `DECODE` table], ), + )), ("MATHEMATICS", ( ("limbs_and_carries", [On limb decomposition and carries], ), ))