WIP: test_1_to_1_transfer
This commit is contained in:
parent
fd007c6625
commit
9e1fb74f93
|
@ -1,8 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Constraint:
|
||||
|
||||
def hash(self) -> bytes:
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,2 @@
|
|||
from .constraint import Constraint
|
||||
from .vacuous import Vacuous
|
|
@ -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()
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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))
|
|
@ -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()
|
|
@ -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)
|
Loading…
Reference in New Issue