drop python cl spec
This commit is contained in:
parent
5ce7b253cf
commit
7ba69caccb
|
@ -1,25 +0,0 @@
|
|||
# Coordination Layer
|
||||
|
||||
This module provides the executable specifications of the Coordination Layer (CL).
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
We are currently experimenting with the Noir Language. In order to run the specification, you will need to install Noir.
|
||||
|
||||
Follow the instructions here:
|
||||
https://noir-lang.org/docs/getting_started/installation/
|
||||
|
||||
Verify the installation by running
|
||||
|
||||
```bash
|
||||
noirup
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
From the repository root run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover -v coordination-layer
|
||||
```
|
|
@ -1,20 +0,0 @@
|
|||
"""
|
||||
This module holds the logic for building and verifying homomorphic balance commitments.
|
||||
"""
|
||||
|
||||
from constraints import Constraint
|
||||
from crypto import Field, Point, prf, hash_to_curve, pederson_commit, _str_to_vec
|
||||
|
||||
|
||||
def balance_commitment(value: Field, blinding: Field, unit: Point):
|
||||
return pederson_commit(value, blinding, unit)
|
||||
|
||||
|
||||
def fungibility_domain(unit: str, birth_cm: Field) -> Point:
|
||||
"""The fungibility domain of this note"""
|
||||
return hash_to_curve("CL_NOTE_NULL", birth_cm, *_str_to_vec(unit))
|
||||
|
||||
|
||||
def blinding(tx_rand: Field, nf_pk: Field) -> Field:
|
||||
"""Blinding factor used in balance commitments"""
|
||||
return prf("CL_NOTE_BAL_BLIND", tx_rand, nf_pk)
|
|
@ -1,3 +0,0 @@
|
|||
from .constraint import Constraint, Proof
|
||||
|
||||
from .vacuous import Vacuous
|
|
@ -1,21 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from .noir_constraint import NoirConstraint, NoirProof
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bigger:
|
||||
"""
|
||||
The statement "I know an `x` that is bigger than `y`".
|
||||
- `y` is a public parameter provided when the constraint is initialized
|
||||
- `x` is a secret parameter provided at proving time
|
||||
"""
|
||||
|
||||
y: int
|
||||
_noir = NoirConstraint("bigger")
|
||||
|
||||
def prove(self, x: int) -> NoirProof:
|
||||
return self._noir.prove({"x": str(x), "y": str(self.y)})
|
||||
|
||||
def verify(self, proof: NoirProof):
|
||||
return self._noir.verify({"y": str(self.y)}, proof)
|
|
@ -1,17 +0,0 @@
|
|||
"""
|
||||
This module defines the Constraint interface.
|
||||
|
||||
Constraints are the predicates that must be satisfied in order to destroy or create a note.
|
||||
|
||||
The logic of a constraint is implemented in a ZK Circuit, and then wrapped in a python interface
|
||||
for interacting with the rest of the the system.
|
||||
"""
|
||||
|
||||
|
||||
class Proof:
|
||||
pass
|
||||
|
||||
|
||||
class Constraint:
|
||||
def hash(self) -> bytes:
|
||||
raise NotImplementedError()
|
|
@ -1,105 +0,0 @@
|
|||
"""
|
||||
This module provides the interface for loading, proving and verifying constraints written in noir.
|
||||
|
||||
The assumptions of this module:
|
||||
- noir constraints are defined as a noir package in `./noir/crates/<constraint>/`
|
||||
- ./noir is relative to this file
|
||||
|
||||
For ergonomics, one should provide python wrappers that understands the API of
|
||||
the corresponding constraint.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import sh
|
||||
import portalocker
|
||||
import tempfile
|
||||
import toml
|
||||
|
||||
|
||||
from constraints import Proof
|
||||
|
||||
NOIR_DIR = Path(__file__).resolve().parent.parent / "noir"
|
||||
LOCK_FILE = NOIR_DIR / ".CL.lock"
|
||||
CONSTRAINTS_DIR = NOIR_DIR / "crates"
|
||||
|
||||
NARGO = sh.Command("nargo")
|
||||
|
||||
|
||||
@dataclass
|
||||
class NoirProof(Proof):
|
||||
proof: str
|
||||
|
||||
|
||||
class NoirConstraint:
|
||||
"""
|
||||
Provides a wrapper around the `nargo` command for interacting with a constraint
|
||||
written in Noir.
|
||||
|
||||
E.g. NoirConstraint("bigger") corresponds to the noir circuit defined
|
||||
in "./noir/crates/bigger/".
|
||||
|
||||
Calling API methods on this object will map to shelling out to `nargo` to execute
|
||||
the relevant `nargo` action.
|
||||
|
||||
Efforts are taken to make this wrapper thread safe.To prevent any race
|
||||
conditions we limit one action at any one time across the `./noir` directory.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
assert self.noir_package_dir.exists() and self.noir_package_dir.is_dir()
|
||||
self._prepare()
|
||||
|
||||
@property
|
||||
def noir_package_dir(self):
|
||||
return CONSTRAINTS_DIR / self.name
|
||||
|
||||
def prove(self, params: dict) -> NoirProof:
|
||||
"""
|
||||
Attempts to prove the noir constraint with the given paramaters.
|
||||
1. Write the paramaters to the noir Prover.toml file
|
||||
2. execute `nargo prove`
|
||||
3. Retreive the proof written by nargo.
|
||||
|
||||
Returns the NoirProof containing the proof of the statment.
|
||||
"""
|
||||
with portalocker.TemporaryFileLock(LOCK_FILE):
|
||||
with open(self.noir_package_dir / "Prover.toml", "w") as prover_f:
|
||||
toml.dump(params, prover_f)
|
||||
|
||||
prove_res = self._nargo("prove", _return_cmd=True)
|
||||
assert prove_res.exit_code == 0
|
||||
|
||||
with open(NOIR_DIR / "proofs" / f"{self.name}.proof", "r") as proof:
|
||||
return NoirProof(proof.read())
|
||||
|
||||
def verify(self, params: dict, proof: NoirProof):
|
||||
"""
|
||||
Attempts to verify a proof given the public paramaters.
|
||||
1. Write the public paramaters to the Verifier.toml
|
||||
2. Write the proof to the location nargo expects it
|
||||
3. Execute `nargo verify`
|
||||
4. Check the process exit code.
|
||||
"""
|
||||
with portalocker.TemporaryFileLock(LOCK_FILE):
|
||||
with open(self.noir_package_dir / "Verifier.toml", "w") as verifier_f:
|
||||
toml.dump(params, verifier_f)
|
||||
|
||||
with open(NOIR_DIR / "proofs" / f"{self.name}.proof", "w") as proof_file:
|
||||
proof_file.write(proof.proof)
|
||||
verify_res = self._nargo("verify", _ok_code=[0, 1], _return_cmd=True)
|
||||
return verify_res.exit_code == 0
|
||||
|
||||
def _nargo(self, *args, **kwargs):
|
||||
return NARGO(*args, **kwargs, _cwd=self.noir_package_dir)
|
||||
|
||||
def _prepare(self):
|
||||
"""
|
||||
Verify that the Noir circuit is well defined.
|
||||
"""
|
||||
check = self._nargo("check", _return_cmd=True)
|
||||
assert check.exit_code == 0
|
||||
compile = self._nargo("compile", _return_cmd=True)
|
||||
assert compile.exit_code == 0
|
|
@ -1,14 +0,0 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from .bigger import Bigger
|
||||
|
||||
|
||||
class TestBigger(TestCase):
|
||||
def test_bigger(self):
|
||||
bigger = Bigger(3)
|
||||
proof = bigger.prove(5)
|
||||
bigger.verify(proof)
|
||||
|
||||
# If we try to reuse the proof for a different Bigger instance, it fails
|
||||
bigger_4 = Bigger(4)
|
||||
assert not bigger_4.verify(proof)
|
|
@ -1,20 +0,0 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from .noir_constraint import NoirConstraint
|
||||
|
||||
|
||||
class TestNoirCoinstraint(TestCase):
|
||||
def test_bigger(self):
|
||||
# simple constraint that proves we know a number bigger than the provided
|
||||
# public input.
|
||||
bigger = NoirConstraint("bigger")
|
||||
|
||||
# x is the secret input, y is the public input
|
||||
proof = bigger.prove({"x": "5", "y": "3"})
|
||||
|
||||
# The proof that we know an `x` that is bigger than `y` should verify
|
||||
# Note, we must provide the public input that was used in the proof.
|
||||
assert bigger.verify({"y": "3"}, proof)
|
||||
|
||||
# If we change the public input, the proof fails to verify.
|
||||
assert not bigger.verify({"y": "4"}, proof)
|
|
@ -1,18 +0,0 @@
|
|||
from constraints import Constraint, Proof
|
||||
from crypto import Field
|
||||
|
||||
|
||||
class Vacuous(Constraint):
|
||||
"""
|
||||
This is the empty constraint, it return true for any proof
|
||||
"""
|
||||
|
||||
def hash(self):
|
||||
# chosen by a fair 2**64 sided die.
|
||||
return Field(14500592324922987342)
|
||||
|
||||
def prove(self) -> Proof:
|
||||
return Proof()
|
||||
|
||||
def verify(self, _proof: Proof):
|
||||
return True
|
|
@ -1,108 +0,0 @@
|
|||
from py_ecc import bn128
|
||||
from keum import grumpkin
|
||||
import poseidon
|
||||
|
||||
|
||||
# !Important! The crypto primitives here must be in agreement with the proving system
|
||||
# E.g. if you are using noir with the Barretenberg, we must use the Grumpkin curve.
|
||||
|
||||
# Point = grumpkin.AffineWeierstrass
|
||||
# Field = grumpkin.Fq
|
||||
|
||||
Point = bn128.FQ
|
||||
Field = bn128.Point2D[FQ]
|
||||
|
||||
|
||||
def fake_algebraic_hash(data) -> Field:
|
||||
"""
|
||||
HACK: we'll fake the algebraic hash using sha256(data) % Field.ORDER
|
||||
"""
|
||||
assert all(isinstance(d, Field) for d in data), f"{data}\n{[type(d) for d in data]}"
|
||||
data = b"".join(d.v.to_bytes(256 // 8) for d in data)
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
return Field(int(sha256(data).hexdigest(), 16))
|
||||
|
||||
|
||||
def build_poseidon():
|
||||
h = poseidon.Poseidon(
|
||||
p=Field.ORDER,
|
||||
security_level=128,
|
||||
alpha=5,
|
||||
input_rate=3,
|
||||
t=9,
|
||||
)
|
||||
|
||||
# TODO: this is hacks on hacks to make poseidon take in arbitrary input length.
|
||||
# Fix is to implement a sponge as described in section 2.1 of
|
||||
# https://eprint.iacr.org/2019/458.pdf
|
||||
def inner(data):
|
||||
|
||||
digest = 0
|
||||
for i in range(0, len(data), h.input_rate - 1):
|
||||
digest = h.run_hash([digest, *data[i : i + h.input_rate - 1]])
|
||||
return Field(int(digest))
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
# HASH = build_poseidon()
|
||||
HASH = fake_algebraic_hash
|
||||
|
||||
|
||||
def prf(domain, *elements) -> Field:
|
||||
return HASH([*_str_to_vec(domain), *elements])
|
||||
|
||||
|
||||
def hash_to_curve(domain, *elements) -> Point:
|
||||
# HACK: we don't currently have a proper hash_to_curve implementation
|
||||
# so we hack the Point.random() function.
|
||||
#
|
||||
# Point.random() calls into the global `random` module to generate a
|
||||
# point. We will seed the random module with the result of hashing the
|
||||
# elements and then call Point.random() to retreive the point
|
||||
# corresponding to the mentioned elements.
|
||||
|
||||
r = prf(f"HASH_TO_CURVE_{domain}", *elements)
|
||||
|
||||
import random
|
||||
|
||||
random.seed(r.v)
|
||||
return Point.random()
|
||||
|
||||
|
||||
def comm(*elements):
|
||||
"""
|
||||
Returns a commitment to the sequence of elements.
|
||||
|
||||
The commitmtent can be opened at index 0..len(elements)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def pederson_commit(value: Field, blinding: Field, domain: Point) -> Point:
|
||||
return Point.generator().mul(value) + domain.mul(blinding)
|
||||
|
||||
|
||||
def merkle_root(data) -> Field:
|
||||
data = _pad_to_power_of_2(data)
|
||||
nodes = [CRH(d) for d in data]
|
||||
while len(nodes) > 1:
|
||||
nodes = [CRH(nodes[i], nodes[i + 1]) for i in range(0, len(nodes), 2)]
|
||||
|
||||
return nodes[0]
|
||||
|
||||
|
||||
def _pad_to_power_of_2(data):
|
||||
import math
|
||||
|
||||
max_lower_bound = int(math.log2(len(data)))
|
||||
if 2**max_lower_bound == len(data):
|
||||
return data
|
||||
to_pad = 2 ** (max_lower_bound + 1) - len(data)
|
||||
return data + [Field.zero()] * to_pad
|
||||
|
||||
|
||||
def _str_to_vec(s):
|
||||
return [Field(ord(c)) for c in s]
|
|
@ -1,4 +0,0 @@
|
|||
crates/*/Prover.toml
|
||||
crates/*/Verifier.toml
|
||||
proofs/*.proof
|
||||
target/*
|
|
@ -1,2 +0,0 @@
|
|||
[workspace]
|
||||
members = ["crates/bigger"]
|
|
@ -1,18 +0,0 @@
|
|||
# Noir Circuits
|
||||
|
||||
this directory holds all the circuits written in Noir, used by the CL specification.
|
||||
|
||||
Each circuit is it's own nargo package under the `crates/` directory.
|
||||
|
||||
## Creating a new circuit
|
||||
|
||||
1. inside `crates/`, run `nargo new <circuit name>`
|
||||
2. update `./Nargo.toml` to include the new circuit in the workspace
|
||||
|
||||
## Testing circuits
|
||||
|
||||
Under `./noir`, simple run.
|
||||
|
||||
```
|
||||
nargo test
|
||||
```
|
|
@ -1,7 +0,0 @@
|
|||
[package]
|
||||
name = "bigger"
|
||||
type = "bin"
|
||||
authors = [""]
|
||||
compiler_version = ">=0.22.0"
|
||||
|
||||
[dependencies]
|
|
@ -1,19 +0,0 @@
|
|||
fn main(x: u32, y: pub u32) {
|
||||
assert(x > y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bigger() {
|
||||
main(3, 2);
|
||||
}
|
||||
|
||||
|
||||
#[test(should_fail)]
|
||||
fn test_equal() {
|
||||
main(2, 2);
|
||||
}
|
||||
|
||||
#[test(should_fail)]
|
||||
fn test_smaller() {
|
||||
main(1, 2);
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from crypto import (
|
||||
Field,
|
||||
Point,
|
||||
prf,
|
||||
pederson_commit,
|
||||
_str_to_vec,
|
||||
merkle_root,
|
||||
hash_to_curve,
|
||||
)
|
||||
|
||||
from constraints import Constraint, Proof
|
||||
|
||||
|
||||
def nf_pk(nf_sk) -> Field:
|
||||
return prf("CL_NOTE_NF", nf_sk)
|
||||
|
||||
|
||||
def balance_commitment(value: Field, tx_rand: Field, funge: Point):
|
||||
return pederson_commit(value, tx_rand, funge)
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class InnerNote:
|
||||
value: Field
|
||||
unit: str
|
||||
# TODO: inner notes should hold commitments to constraints.
|
||||
# Constraints themselves will be stored in a key-value store
|
||||
birth_constraint: Constraint
|
||||
death_constraints: list[Constraint]
|
||||
state: Field
|
||||
nonce: Field
|
||||
rand: Field # source of randomness for note commitment
|
||||
|
||||
def __post_init__(self):
|
||||
if isinstance(self.value, int):
|
||||
self.value = Field(self.value)
|
||||
assert isinstance(self.value, Field), f"value is {type(self.value)}"
|
||||
assert isinstance(self.unit, str), f"unit is {type(self.unit)}"
|
||||
assert isinstance(
|
||||
self.birth_constraint, Constraint
|
||||
), f"birth_constraint is {type(self.birth_constraint)}"
|
||||
assert isinstance(
|
||||
self.death_constraints, list
|
||||
), f"death_constraints is {type(self.death_constraints)}"
|
||||
assert all(
|
||||
isinstance(d, Constraint) for d in self.death_constraints
|
||||
), f"{[type(d) for d in self.death_constraints]}"
|
||||
assert isinstance(self.state, Field), f"state is {type(self.state)}"
|
||||
assert isinstance(self.nonce, Field), f"nonce is {type(self.nonce)}"
|
||||
assert isinstance(self.rand, Field), f"rand is {type(self.rand)}"
|
||||
|
||||
def verify_death(self, death_cm: Field, death_proof: Proof) -> bool:
|
||||
constraint = [d for d in self.death_constraints if d.hash() == death_cm]
|
||||
if len(constraint) == 0:
|
||||
# given commitment was not one of the allowed death constraints
|
||||
return False
|
||||
|
||||
constraint = constraint[0]
|
||||
|
||||
# TODO: verifying the death constraint should include a commitment to the
|
||||
# partial transaction so that the death constraint can make statements
|
||||
# regarding the entire transaction.
|
||||
return constraint.verify(death_proof)
|
||||
|
||||
def verify_birth(self, birth_proof: Proof) -> bool:
|
||||
# TODO: Should verifying the birth constraint include a commitment
|
||||
# to the partial transaction?
|
||||
return self.birth_constraint.verify(birth_proof)
|
||||
|
||||
def verify_value(self) -> bool:
|
||||
return 0 <= self.value and value <= 2**64
|
||||
|
||||
def r(self, index: int):
|
||||
return prf("CL_NOTE_COMM_RAND", self.rand, index)
|
||||
|
||||
@property
|
||||
def fungibility_domain(self) -> Point:
|
||||
"""The fungibility domain of this note"""
|
||||
return hash_to_curve(
|
||||
"CL_NOTE_NULL", self.birth_constraint.hash(), *_str_to_vec(self.unit)
|
||||
)
|
||||
|
||||
def death_constraints_root(self) -> Field:
|
||||
"""
|
||||
Returns the merkle root over the set of death constraints
|
||||
"""
|
||||
return merkle_root(self.death_constraints)
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class PublicNote:
|
||||
note: InnerNote
|
||||
nf_pk: Field
|
||||
|
||||
def blinding(self, tx_rand: Field) -> Field:
|
||||
"""Blinding factor used in balance commitments"""
|
||||
return prf("CL_NOTE_BAL_BLIND", tx_rand, self.note.nonce, self.nf_pk)
|
||||
|
||||
def balance(self, tx_rand):
|
||||
"""
|
||||
Returns the pederson commitment to the notes value.
|
||||
"""
|
||||
return balance_commitment(
|
||||
self.note.value, self.blinding(tx_rand), self.note.fungibility_domain
|
||||
)
|
||||
|
||||
def zero(self, tx_rand):
|
||||
"""
|
||||
Returns the pederson commitment to the notes value.
|
||||
"""
|
||||
return balance_commitment(
|
||||
Field.zero(), self.blinding(tx_rand), self.note.fungibility_domain
|
||||
)
|
||||
|
||||
def commit(self) -> Field:
|
||||
# blinding factors between data elems ensure no information is leaked in merkle paths
|
||||
return merkle_root(
|
||||
self.note.r(0),
|
||||
self.note.birth_constraint.hash(),
|
||||
self.note.r(1),
|
||||
self.note.death_constraints_root(),
|
||||
self.note.r(2),
|
||||
self.note.value,
|
||||
self.note.r(3),
|
||||
self.note.unit,
|
||||
self.note.r(4),
|
||||
self.note.state,
|
||||
self.note.r(5),
|
||||
self.note.nonce,
|
||||
self.note.r(6),
|
||||
self.nf_pk,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class SecretNote:
|
||||
note: InnerNote
|
||||
nf_sk: Field
|
||||
|
||||
def to_public(self) -> PublicNote:
|
||||
return PublicNote(note=self.note, nf_pk=nf_pk(self.nf_sk))
|
||||
|
||||
def nullifier(self):
|
||||
"""
|
||||
The nullifier that must be provided when spending this note along
|
||||
with a proof that the nf_sk used to compute the nullifier corresponds
|
||||
to the nf_pk in the public note commitment.
|
||||
"""
|
||||
return prf("NULLIFIER", self.nonce, self.nf_sk)
|
|
@ -1,146 +0,0 @@
|
|||
* Open Issues
|
||||
|
||||
** Do we need both Public, Secret and Inner Note?
|
||||
|
||||
Can't we simply have a Note (currently InnerNote) and the partial transaction will introduce
|
||||
a wrapper over the Note struct that will have the extra fields necessary for using notes within
|
||||
a transaction system:
|
||||
|
||||
#+begin_src python
|
||||
|
||||
|
||||
# ----- note.py -----
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Note:
|
||||
value: Field
|
||||
unit: str
|
||||
birth_constraint: Constraint
|
||||
death_constraints: list[Constraint]
|
||||
state: Field
|
||||
nonce: Field
|
||||
rand: Field
|
||||
|
||||
def verify_death(self, death_cm: Field, death_proof: Proof) -> bool:
|
||||
pass
|
||||
|
||||
def verify_birth(self, birth_proof: Proof) -> bool:
|
||||
pass
|
||||
|
||||
def verify_value(self) -> bool:
|
||||
pass
|
||||
|
||||
def fungibility_domain(self) -> Field:
|
||||
pass
|
||||
|
||||
# ----- partial_transaction.py -----
|
||||
|
||||
@dataclass
|
||||
class InputNote:
|
||||
note: Note
|
||||
nullifier: Note
|
||||
death_cm: Field
|
||||
death_proof: Proof
|
||||
balance_cm: Point
|
||||
|
||||
@dataclass
|
||||
class OutputNote:
|
||||
note: PublicNote
|
||||
birth_proof: Proof
|
||||
balance_cm: Point
|
||||
|
||||
#+end_src
|
||||
|
||||
|
||||
** provided commitment to zero may removing the blinding of the pederson commitment
|
||||
|
||||
Since you can subtract the randomness from the commitment to get just the binding part.
|
||||
|
||||
Ok, lets deal with these zero commitments.
|
||||
|
||||
In place of zero commitments, we can have the solver prove that the value part of a pederson
|
||||
commitment is zero.
|
||||
|
||||
|
||||
|
||||
** Rename transaction randonmness to tx_rand and commitment randomness to cm_rand
|
||||
|
||||
Currently they are both called "rand" and it's confusing.
|
||||
|
||||
** Why does PartialTransaction have a "blinding"?
|
||||
|
||||
I believe it should only have a derived balance field.
|
||||
|
||||
** Poseidon Constants for Grumpkin Field
|
||||
|
||||
Generating the poseidon constants for the Grumpkin curve is very slow, like takes 1 minute.
|
||||
|
||||
I need to pre-generate these to make the curve work.
|
||||
|
||||
- I tried the other large curve where the params are already generated and it is still slow. Need to dig into this.
|
||||
|
||||
** Solvers need the value of a note in order to solve it
|
||||
|
||||
- do users provide a merkle proof showing the value is legit?
|
||||
|
||||
- merkle proofs also reveal positional information.
|
||||
- if we are to reveal partial information to solvers
|
||||
we will need to blind the other leaf nodes of the tree:
|
||||
|
||||
Note = VALUE || UNIT || ...
|
||||
|
||||
com = ..root..
|
||||
/ \
|
||||
h(h(V), h(U)) ...
|
||||
/ \
|
||||
h(VALUE) h(UNIT)
|
||||
| |
|
||||
VALUE UNIT
|
||||
|
||||
Revealing the value's merkle path would reveal h(UNIT) and since UNIT is well known, h(UNIT) is also easily computable.
|
||||
|
||||
Thus each component of the Note should have a blinding factor
|
||||
Note = (r1, VALUE) || (r1, UNIT) || ....
|
||||
|
||||
** Transferring requires recipients sk
|
||||
|
||||
We need to be able to create a partial transaction where we don't know the output note's nullifier.
|
||||
|
||||
What we need: A public note + balance commitment
|
||||
|
||||
balance commitment is computed as
|
||||
|
||||
```
|
||||
value <- known by transaction builder
|
||||
unit <- known by transaction builder
|
||||
blinding <- PRF_r(nullifier) !!!! <-- this is where the issue is
|
||||
funge <- hash_to_curve(Unit)
|
||||
balance_commit <- pedersen_commit(value, blinding, funge)
|
||||
```
|
||||
|
||||
Why is the blinding a PRF of the nullifier? Can't we simply use the randomness of the transaction to derive a blinding factor?
|
||||
|
||||
*** How does zcash solve this?
|
||||
|
||||
They provide specific randomness for this when building the transaction.
|
||||
|
||||
|
||||
** Note Unit should be a string hashed to curve point when needed.
|
||||
We want human readable units (i.e. "ETH" "NMO")
|
||||
|
||||
I've gone and denoted it as bytes.
|
||||
|
||||
|
||||
* Solved
|
||||
** Note.verify() should take a proof
|
||||
|
||||
We should have two variants, verifyDeath(deathProof) verifyBirth(birthProof)
|
||||
|
||||
|
||||
** Do we need note.randomness?
|
||||
|
||||
It's currently used in the note commitment. But perhaps it's not really necessary?
|
||||
|
||||
- it allows you to get away form content based addressing, this may be necessary for total privacy.
|
||||
|
||||
Yes, we need this
|
|
@ -1,62 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
from constraints import Proof
|
||||
from note import PublicNote, SecretNote
|
||||
from crypto import Field, Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputNote:
|
||||
note: SecretNote
|
||||
death_cm: Field # commitment to the death constraint we are using
|
||||
death_proof: Proof
|
||||
|
||||
def verify(self):
|
||||
# TODO: note.note is ugly
|
||||
return self.note.note.verify_death(self.death_cm, self.death_proof)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputNote:
|
||||
note: PublicNote
|
||||
birth_proof: Proof
|
||||
|
||||
def verify(self):
|
||||
# TODO: note.note is ugly
|
||||
return self.note.note.verify_birth(self.birth_proof)
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class PartialTransaction:
|
||||
inputs: list[InputNote]
|
||||
outputs: list[OutputNote]
|
||||
rand: Field
|
||||
|
||||
def verify(self) -> bool:
|
||||
valid_inputs = all(i.verify() for i in self.inputs)
|
||||
valid_outputs = all(o.verify() for o in self.outputs)
|
||||
return valid_inputs and valid_outputs
|
||||
|
||||
def balance(self) -> Point:
|
||||
output_balance = sum(
|
||||
(n.note.balance(self.rand) for n in self.outputs),
|
||||
start=Point.zero(),
|
||||
)
|
||||
input_balance = sum(
|
||||
(n.note.to_public().balance(self.rand) for n in self.inputs),
|
||||
start=Point.zero(),
|
||||
)
|
||||
return output_balance + input_balance.negate()
|
||||
|
||||
def zero(self) -> Field:
|
||||
output_zero = sum(
|
||||
(n.note.zero(self.rand) for n in self.outputs),
|
||||
start=Point.zero(),
|
||||
)
|
||||
input_zero = sum(
|
||||
(n.note.to_public().zero(self.rand) for n in self.inputs),
|
||||
start=Point.zero(),
|
||||
)
|
||||
|
||||
return output_zero + input_zero.negate()
|
|
@ -1,23 +0,0 @@
|
|||
"""
|
||||
This module maintains the state of the CL.
|
||||
|
||||
Namely we are interested in:
|
||||
- the set of note commitments
|
||||
- the set of note nullifiers (spent notes)
|
||||
- the set of constraints
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import note
|
||||
import constraint
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
# commitments: set[note.Commitment]
|
||||
# nullifiers: set[note.Nullifier]
|
||||
# constraints: dict[bytes, constraint.Constraint]
|
||||
|
||||
def add_constraint(self, c: constraint.Constraint):
|
||||
self.constraints[c.hash()] = c
|
|
@ -1,30 +0,0 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from hypothesis import example, given, settings, strategies as st
|
||||
|
||||
from crypto import Field, hash_to_curve
|
||||
from balance_commitment import balance_commitment
|
||||
|
||||
|
||||
@st.composite
|
||||
def field(draw):
|
||||
x = draw(st.integers(min_value=0, max_value=Field.ORDER - 1))
|
||||
return Field(x)
|
||||
|
||||
|
||||
@st.composite
|
||||
def point(draw):
|
||||
x = draw(field())
|
||||
return hash_to_curve("T", x)
|
||||
|
||||
|
||||
class TestBalanceCommitment(TestCase):
|
||||
@given(r=field(), a=field(), b=field(), unit=point())
|
||||
@settings(max_examples=3)
|
||||
def test_value_additive(self, r, a, b, unit):
|
||||
print(r, a, b, unit)
|
||||
b1 = balance_commitment(r, a, unit)
|
||||
b2 = balance_commitment(r, b, unit)
|
||||
b3 = balance_commitment(r, a + b, unit)
|
||||
|
||||
assert b1 + b2 == b3
|
|
@ -1,33 +0,0 @@
|
|||
"""
|
||||
This module tests that all the hacks we introduced in our crypto mocks give us
|
||||
the basic behaviour that we need.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
from crypto import Field, Point, hash_to_curve, prf
|
||||
|
||||
|
||||
class TestCrypto(TestCase):
|
||||
def test_hash_to_curve(self):
|
||||
p1 = hash_to_curve("TEST", Field(0), Field(1), Field(2))
|
||||
p2 = hash_to_curve("TEST", Field(0), Field(1), Field(2))
|
||||
|
||||
assert isinstance(p1, Point)
|
||||
|
||||
assert p1 == p2
|
||||
|
||||
p3 = hash_to_curve("TEST", Field(0), Field(1), Field(3))
|
||||
|
||||
assert p1 != p3
|
||||
|
||||
def test_prf(self):
|
||||
r1 = prf("TEST", Field(0), Field(1), Field(2))
|
||||
r2 = prf("TEST", Field(0), Field(1), Field(2))
|
||||
|
||||
assert isinstance(r1, Field)
|
||||
assert r1 == r2
|
||||
|
||||
r3 = prf("TEST", Field(0), Field(1), Field(3))
|
||||
assert r1 != r3
|
|
@ -1,73 +0,0 @@
|
|||
from unittest import TestCase
|
||||
from dataclasses import dataclass
|
||||
|
||||
from crypto import Field, prf
|
||||
from note import InnerNote, PublicNote, SecretNote, nf_pk
|
||||
from partial_transaction import PartialTransaction, InputNote, OutputNote
|
||||
from transaction_bundle import TransactionBundle
|
||||
|
||||
from constraints import Vacuous
|
||||
|
||||
|
||||
class TestTransfer(TestCase):
|
||||
def test_1_to_1_transfer(self):
|
||||
# Alice wants to transfer ownership of a note to Bob.
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
sk: Field
|
||||
|
||||
@property
|
||||
def pk(self) -> Field:
|
||||
return nf_pk(self.sk)
|
||||
|
||||
alice = User(sk=Field.random())
|
||||
bob = User(sk=Field.random())
|
||||
|
||||
alices_note = SecretNote(
|
||||
note=InnerNote(
|
||||
value=100,
|
||||
unit="NMO",
|
||||
birth_constraint=Vacuous(),
|
||||
death_constraints=[Vacuous()],
|
||||
state=Field.zero(),
|
||||
nonce=Field.random(),
|
||||
rand=Field.random(),
|
||||
),
|
||||
nf_sk=alice.sk,
|
||||
)
|
||||
|
||||
tx_rand = Field.random()
|
||||
bobs_note = PublicNote(
|
||||
note=InnerNote(
|
||||
value=100,
|
||||
unit="NMO",
|
||||
birth_constraint=Vacuous(),
|
||||
death_constraints=[Vacuous()],
|
||||
state=Field.zero(),
|
||||
nonce=Field.random(),
|
||||
rand=Field.random(),
|
||||
),
|
||||
nf_pk=bob.pk,
|
||||
)
|
||||
|
||||
ptx = PartialTransaction(
|
||||
inputs=[
|
||||
InputNote(
|
||||
note=alices_note,
|
||||
death_cm=Vacuous().hash(),
|
||||
death_proof=Vacuous().prove(),
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
OutputNote(
|
||||
note=bobs_note,
|
||||
birth_proof=Vacuous().prove(),
|
||||
)
|
||||
],
|
||||
rand=tx_rand,
|
||||
)
|
||||
|
||||
bundle = TransactionBundle(bundle=[ptx])
|
||||
|
||||
assert bundle.verify()
|
|
@ -1,20 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from partial_transaction import PartialTransaction
|
||||
from crypto import Field, Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionBundle:
|
||||
bundle: list[PartialTransaction]
|
||||
|
||||
def is_balanced(self) -> bool:
|
||||
# TODO: move this to a NOIR constraint
|
||||
balance_commitment = sum(
|
||||
(ptx.balance() + ptx.zero().negate() for ptx in self.bundle),
|
||||
start=Point.zero(),
|
||||
)
|
||||
return Point.zero() == balance_commitment
|
||||
|
||||
def verify(self) -> bool:
|
||||
return self.is_balanced() and all(ptx.verify() for ptx in self.bundle)
|
Loading…
Reference in New Issue