WIP: test_1_to_1_transfer

This commit is contained in:
David Rusu 2024-05-27 18:58:45 +04:00
parent fd007c6625
commit 9e1fb74f93
10 changed files with 326 additions and 46 deletions

View File

@ -1,8 +0,0 @@
from dataclasses import dataclass
@dataclass
class Constraint:
def hash(self) -> bytes:
raise NotImplementedError()

View File

@ -0,0 +1,2 @@
from .constraint import Constraint
from .vacuous import Vacuous

View File

@ -0,0 +1,15 @@
"""
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.
"""
from dataclasses import dataclass
class Constraint:
def hash(self) -> bytes:
raise NotImplementedError()

View File

@ -0,0 +1,15 @@
from noir_constraint import NoirProof
from constraints import Constraint
class Vacuous(Constraint):
"""
This is the empty constraint, it return true for any proof
"""
def prove(self) -> NoirProof:
return NoirProof("vacuous")
def verify(self, _proof: NoirProof):
return True

View File

@ -1,4 +1,4 @@
from keum import grumpkin
from keum import grumpkin, PrimeFiniteField
import poseidon
@ -9,6 +9,10 @@ Point = grumpkin.AffineWeierstrass
Field = grumpkin.Fq
class Field(PrimeFiniteField):
ORDER = poseidon.prime_64
def poseidon_grumpkin_field():
# TODO: These parameters are made up.
# return poseidon.Poseidon(
@ -19,11 +23,23 @@ def poseidon_grumpkin_field():
# t=9,
# )
h, _ = poseidon.case_simple()
# h, _ = poseidon.case_neptune()
# h = poseidon.Poseidon(
# p=Field.ORDER,
# security_level=128,
# alpha=5,
# input_rate=3,
# t=9,
# )
# TODO: this is a hack to make poseidon take in arbitrary input length.
# 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):
assert all(
isinstance(d, Field) for d in data
), f"{data}\n{[type(d) for d in data]}"
data = [d.v for d in 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]])
@ -36,7 +52,7 @@ POSEIDON = poseidon_grumpkin_field()
def prf(domain, *elements):
return POSEIDON(str_to_vec(domain) + elements)
return POSEIDON([*_str_to_vec(domain), *elements])
def comm(*elements):
@ -48,6 +64,10 @@ def comm(*elements):
raise NotImplementedError()
def pederson_commit(value: Field, blinding: Field, domain: Point) -> Point:
return value * Point.generator() + blinding * domain
def merkle_root(data) -> Field:
data = _pad_to_power_of_2(data)
nodes = [CRH(d) for d in data]
@ -64,8 +84,8 @@ def _pad_to_power_of_2(data):
if 2**max_lower_bound == len(data):
return data
to_pad = 2 ** (max_lower_bound + 1) - len(data)
return data + [0] * to_pad
return data + [Field.zero()] * to_pad
def _str_to_vec(s):
return list(map(ord, s))
return [Field(ord(c)) for c in s]

View File

@ -1,11 +1,15 @@
from dataclasses import dataclass
from crypto import Field, Point
from crypto import Field, Point, prf
from constraints import Constraint
@dataclass
class Commitment:
cm: bytes
class NoteCommitment:
cm: Field
blinding: Field
zero: Field
@dataclass
@ -13,47 +17,98 @@ class Nullifier:
nf: bytes
@dataclass
class SecretNote:
note: InnerNote
nf_sk: Field
def to_public_note(self) -> PublicNote:
return PublicNote(
note=self.note,
nf_pk=Point.generator().mul(self.nf_sk),
)
def nf_pk(nf_sk) -> Field:
return prf("CL_NOTE_NF", nf_sk)
@dataclass
class PublicNote:
note: InnerNote
nf_pk: Point
def commit(self) -> Commitment:
return crypto.COMM(
self.note.birth_constraint.hash(),
self.note.death_constraints_root(),
self.note.value,
self.note.unit,
self.note.state,
self.note.nonce,
self.nf_pk,
)
@dataclass
@dataclass(unsafe_hash=True)
class InnerNote:
value: Field
unit: str
birth_constraint: Constraint
death_constraints: set[Constraint]
death_constraints: list[Constraint]
state: Field
nonce: Field
rand: Field
rand: Field # source of randomness for note commitment
def r(self, index: int):
prf("CL_NOTE_COMM_RAND", self.rand, index)
def verify_value(self) -> bool:
return 0 <= self.value and value <= 2**64
@property
def fungibility_domain(self) -> Field:
"""The fungibility domain of this note"""
return crypto.prf(
"CL_NOTE_NULL", self.birth_constraint.hash(), *crypto.str_to_vec(unit)
)
def death_constraints_root(self) -> Field:
"""
Returns the merkle root over the set of death constraints
"""
return crypto.merkle_root(self.death_constraints)
@dataclass(unsafe_hash=True)
class PublicNote:
note: InnerNote
nf_pk: Field
def blinding(self, rand: Field) -> Field:
"""Blinding factor used in balance commitments"""
return prf("CL_NOTE_BAL_BLIND", rand, self.nonce, self.nf_pk)
def commit(self) -> Field:
# blinding factors between data elems ensure no information is leaked in merkle paths
return crypto.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_note(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)
def balance(self, rand):
"""
Returns the pederson commitment to the notes value.
"""
return crypto.pederson_commit(
self.note.value, self.blinding(rand), self.note.fungibility_domain
)
def zero(self, rand):
"""
Returns the pederson commitment to zero using the same blinding as the balance
commitment.
"""
return crypto.pederson_commit(
0, self.blinding(rand), self.note.fungibility_domain
)

View File

@ -0,0 +1,73 @@
* Open Issues
** 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.
** 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
** 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

View File

@ -0,0 +1,31 @@
from dataclasses import dataclass
from note import PublicNote, SecretNote
from crypto import Field, Point
@dataclass
class Output:
note: PublicNote
# pre-computed balance and zero commitment "SecretNote" here.
balance: Field
zero: Field
@dataclass(unsafe_hash=True)
class PartialTransaction:
inputs: list[SecretNote]
outputs: list[Output]
rand: Field
def balance(self) -> Point:
output_balance = sum(n.balance for n in self.outputs)
input_balance = sum(n.note.balance() for n in self.inputs)
return output_balance - input_balance
def blinding(self) -> Field:
return sum(outputs.blinding(self.rand)) - sum(outputs.blinding(self.rand))
def zero(self) -> Field:
return sum(outputs.note.zero(self.rand)) - sum(inputs.zero(self.rand))

View File

@ -0,0 +1,61 @@
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
from transaction_bundle import TransactionBundle
import constraints
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=constraints.Vacuous(),
death_constraints=[constraints.Vacuous()],
state=Field.zero(),
nonce=Field.random(),
rand=Field.random(),
),
nf_sk=alice.sk,
)
bobs_note = PublicNote(
note=InnerNote(
value=100,
unit="NMO",
birth_constraint=constraints.Vacuous(),
death_constraints=[constraints.Vacuous()],
state=Field.zero(),
nonce=Field.random(),
rand=Field.random(),
),
nf_pk=bob.pk,
)
ptx = PartialTransaction(
inputs=[alices_note],
outputs=[alices_note],
rand=Field.random(),
)
bundle = TransactionBundle(bundle=[ptx])
assert bundle.verify()

View File

@ -0,0 +1,16 @@
from dataclasses import dataclass
from partial_transaction import PartialTransaction
from crypto import Field
@dataclass
class TransactionBundle:
bundle: list[PartialTransaction]
def is_balanced(self) -> bool:
# TODO: move this to a NOIR constraint
return Field.zero() == sum(ptx.balance() - ptx.zero() for ptx in self.bundle)
def verify(self) -> bool:
return self.is_balanced() and all(ptx.verify() for ptx in self.bundle)