Skip to content

Commit 7587af6

Browse files
committed
feat(zkatsnark): add commitment for token types in actions
Signed-off-by: Ankit Basu <ankitbasu14@gmail.com>
1 parent b1fef55 commit 7587af6

31 files changed

Lines changed: 615 additions & 234 deletions

x/token/core/zkatsnark/circuit/migration.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import (
2323
// zkatsnark token (MiMC commitment + Jubjub value commitment) has been
2424
// correctly created for the same value, in zero knowledge.
2525
//
26-
// It enforces four constraint groups:
26+
// It enforces five constraint groups:
2727
// 1. Pedersen opening: CommitmentPedersen == TokenTypePed·G[0] + Value·G[1] + RandomnessPed·G[2]
2828
// 2. MiMC commitment: CommitmentMiMC == MiMC(Value, TokenType, RandomnessNew)
2929
// 3. Value commitment: (ValueCommitOutX, ValueCommitOutY) == Value·V + RCV·R
3030
// 4. Range check: Value ∈ [1, 2^MaxBits)
31+
// 5. Type commitment: TypeCommitment == MiMC(TokenType, TypeRandomness)
3132
//
3233
// The shared Value witness variable across groups 1–3 is the structural
3334
// binding that proves the same denomination carries over (Decision C:
@@ -56,17 +57,22 @@ type MigrationCircuit struct {
5657
// commitment.
5758
ValueCommitOutY frontend.Variable `gnark:",public"`
5859

59-
// TokenType is the canonical field-element encoding of the token type,
60-
// produced by EncodeTokenType (SetBytes). Public for type-homogeneity
61-
// checks, consistent with SpendCircuit/OutputCircuit.
62-
TokenType frontend.Variable `gnark:",public"`
60+
// TypeCommitment is the hiding commitment to the token type:
61+
// MiMC(EncodeTokenType(tokenType), TypeRandomness). Public for
62+
// type-homogeneity checks, consistent with SpendCircuit/OutputCircuit.
63+
TypeCommitment frontend.Variable `gnark:",public"`
6364

6465
// ── Private inputs ──────────────────────────────────────────────────
6566

66-
// Value is the token denomination, shared across all four constraint
67+
// Value is the token denomination, shared across all five constraint
6768
// groups. This shared variable is the conservation proof.
6869
Value frontend.Variable
6970

71+
// TokenType is the canonical field-element encoding of the token type,
72+
// produced by EncodeTokenType (SetBytes). Private — the validator
73+
// only sees the TypeCommitment, not the plaintext type.
74+
TokenType frontend.Variable
75+
7076
// TokenTypePed is the zkatdlog encoding of the token type:
7177
// HashToZr(tokenType) = SHA256(tokenType) mod r. This is a DIFFERENT
7278
// encoding from the public TokenType field (which uses EncodeTokenType).
@@ -84,6 +90,10 @@ type MigrationCircuit struct {
8490
// this proof.
8591
RCV frontend.Variable
8692

93+
// TypeRandomness is the randomness used to hide the token type in
94+
// the TypeCommitment. Shared across all descriptions in one action.
95+
TypeRandomness frontend.Variable
96+
8797
// ── Compile-time parameters ─────────────────────────────────────────
8898

8999
// MaxBits is the bit-width of the value range constraint, sourced from
@@ -223,5 +233,17 @@ func (c *MigrationCircuit) Define(api frontend.API) error {
223233
_ = api.ToBinary(c.Value, maxBits)
224234
api.AssertIsDifferent(c.Value, 0)
225235

236+
// ── Constraint Group 5: Type Commitment Integrity ───────────────────
237+
// Enforce: TypeCommitment == MiMC(TokenType, TypeRandomness)
238+
//
239+
// The same TokenType private variable used in Group 2 (MiMC commitment)
240+
// appears here, so the ZK proof structurally binds the committed type
241+
// to the type encoded in the note commitment.
242+
tc, err := gadgets.HashCircuit(api, c.TokenType, c.TypeRandomness)
243+
if err != nil {
244+
return err
245+
}
246+
api.AssertIsEqual(tc, c.TypeCommitment)
247+
226248
return nil
227249
}

x/token/core/zkatsnark/circuit/migration_test.go

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,14 @@ func setupMigration(t *testing.T) {
106106
// ── Test inputs ─────────────────────────────────────────────────────────────
107107

108108
type migrationInputs struct {
109-
value uint64
110-
tokenType string
111-
tokenTypeFr fr.Element // EncodeTokenType(tokenType)
112-
tokenTypePed fr.Element // HashToZr(tokenType) for Pedersen
113-
randomnessPed fr.Element // Pedersen blinding factor
114-
randomnessNew fr.Element // MiMC randomness
115-
rcv fr.Element // Jubjub value-commitment randomness
109+
value uint64
110+
tokenType string
111+
tokenTypeFr fr.Element // EncodeTokenType(tokenType)
112+
tokenTypePed fr.Element // HashToZr(tokenType) for Pedersen
113+
randomnessPed fr.Element // Pedersen blinding factor
114+
randomnessNew fr.Element // MiMC randomness
115+
rcv fr.Element // Jubjub value-commitment randomness
116+
typeRandomness fr.Element // TypeCommitment randomness
116117
}
117118

118119
func newRandomMigrationInputs(t *testing.T) migrationInputs {
@@ -131,22 +132,25 @@ func newRandomMigrationInputs(t *testing.T) migrationInputs {
131132
var tokenTypePed fr.Element
132133
tokenTypePed.SetBigInt(digestBig)
133134

134-
var randomnessPed, randomnessNew, rcv fr.Element
135+
var randomnessPed, randomnessNew, rcv, typeRandomness fr.Element
135136
_, err := randomnessPed.SetRandom()
136137
require.NoError(t, err)
137138
_, err = randomnessNew.SetRandom()
138139
require.NoError(t, err)
139140
_, err = rcv.SetRandom()
140141
require.NoError(t, err)
142+
_, err = typeRandomness.SetRandom()
143+
require.NoError(t, err)
141144

142145
return migrationInputs{
143-
value: value,
144-
tokenType: tokenType,
145-
tokenTypeFr: tokenTypeFr,
146-
tokenTypePed: tokenTypePed,
147-
randomnessPed: randomnessPed,
148-
randomnessNew: randomnessNew,
149-
rcv: rcv,
146+
value: value,
147+
tokenType: tokenType,
148+
tokenTypeFr: tokenTypeFr,
149+
tokenTypePed: tokenTypePed,
150+
randomnessPed: randomnessPed,
151+
randomnessNew: randomnessNew,
152+
rcv: rcv,
153+
typeRandomness: typeRandomness,
150154
}
151155
}
152156

@@ -210,20 +214,26 @@ func buildMigrationAssignment(t *testing.T, inp migrationInputs) *circuit.Migrat
210214
cv, err := jubjub.ValueCommit(inp.value, inp.rcv)
211215
require.NoError(t, err, "jubjub.ValueCommit failed")
212216

217+
// Compute type commitment: MiMC(TokenType, TypeRandomness)
218+
tc, err := mimc.Hash(inp.tokenTypeFr, inp.typeRandomness)
219+
require.NoError(t, err, "mimc.Hash failed for type commitment")
220+
213221
return &circuit.MigrationCircuit{
214222
// Public inputs
215223
CommitmentPedersenX: emulated.ValueOf[emulated.BLS12381Fp](pedXBig),
216224
CommitmentPedersenY: emulated.ValueOf[emulated.BLS12381Fp](pedYBig),
217225
CommitmentMiMC: cm,
218226
ValueCommitOutX: cv.X,
219227
ValueCommitOutY: cv.Y,
220-
TokenType: inp.tokenTypeFr,
228+
TypeCommitment: tc,
221229
// Private inputs
222-
Value: vField,
223-
TokenTypePed: inp.tokenTypePed,
224-
RandomnessPed: inp.randomnessPed,
225-
RandomnessNew: inp.randomnessNew,
226-
RCV: inp.rcv,
230+
Value: vField,
231+
TokenType: inp.tokenTypeFr,
232+
TokenTypePed: inp.tokenTypePed,
233+
RandomnessPed: inp.randomnessPed,
234+
RandomnessNew: inp.randomnessNew,
235+
RCV: inp.rcv,
236+
TypeRandomness: inp.typeRandomness,
227237
// Compile-time parameters
228238
MaxBits: params.DefaultMaxBits,
229239
PedG0X: testPedGens.G0X, PedG0Y: testPedGens.G0Y,
@@ -410,6 +420,10 @@ func TestMigrationCircuitInvalid_OverflowValue(t *testing.T) {
410420
cm, err := mimc.Hash(overflowValue, inp.tokenTypeFr, inp.randomnessNew)
411421
require.NoError(t, err)
412422

423+
// Compute type commitment
424+
tc, err := mimc.Hash(inp.tokenTypeFr, inp.typeRandomness)
425+
require.NoError(t, err)
426+
413427
pedXBig := pedCommit.X.BigInt(new(big.Int))
414428
pedYBig := pedCommit.Y.BigInt(new(big.Int))
415429

@@ -426,12 +440,14 @@ func TestMigrationCircuitInvalid_OverflowValue(t *testing.T) {
426440
CommitmentMiMC: cm,
427441
ValueCommitOutX: fakeCVX,
428442
ValueCommitOutY: fakeCVY,
429-
TokenType: inp.tokenTypeFr,
443+
TypeCommitment: tc,
430444
Value: overflowValue,
445+
TokenType: inp.tokenTypeFr,
431446
TokenTypePed: inp.tokenTypePed,
432447
RandomnessPed: inp.randomnessPed,
433448
RandomnessNew: inp.randomnessNew,
434449
RCV: inp.rcv,
450+
TypeRandomness: inp.typeRandomness,
435451
MaxBits: params.DefaultMaxBits,
436452
PedG0X: testPedGens.G0X, PedG0Y: testPedGens.G0Y,
437453
PedG1X: testPedGens.G1X, PedG1Y: testPedGens.G1Y,
@@ -445,3 +461,22 @@ func TestMigrationCircuitInvalid_OverflowValue(t *testing.T) {
445461
require.Error(t, err,
446462
"MigrationCircuit must reject values >= 2^MaxBits, overflow attack possible if not")
447463
}
464+
465+
func TestMigrationCircuitInvalid_WrongTypeRandomness(t *testing.T) {
466+
setupMigration(t)
467+
468+
inp := newRandomMigrationInputs(t)
469+
assignment := buildMigrationAssignment(t, inp)
470+
471+
var wrongTR fr.Element
472+
_, err := wrongTR.SetRandom()
473+
require.NoError(t, err)
474+
assignment.TypeRandomness = wrongTR
475+
476+
witness, err := frontend.NewWitness(assignment, ecc.BLS12_381.ScalarField())
477+
require.NoError(t, err)
478+
479+
err = migrationCS.IsSolved(witness)
480+
require.Error(t, err,
481+
"MigrationCircuit must reject a witness where TypeRandomness doesn't produce the public TypeCommitment")
482+
}

x/token/core/zkatsnark/circuit/output.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@ import (
1616

1717
// OutputCircuit proves that a newly created output token is well-formed.
1818
//
19-
// It enforces three constraint groups:
19+
// It enforces four constraint groups:
2020
// 1. CommitmentOut == MiMC(Value, TokenType, Randomness)
2121
// 2. (ValueCommitOutX, ValueCommitOutY) == Value·V + RCV·R
2222
// 3. Value ∈ [1, 2^MaxBits)
23+
// 4. TypeCommitment == MiMC(TokenType, TypeRandomness)
2324
type OutputCircuit struct {
2425
// Public inputs
2526
CommitmentOut frontend.Variable `gnark:"public"`
2627
ValueCommitOutX frontend.Variable `gnark:"public"`
2728
ValueCommitOutY frontend.Variable `gnark:"public"`
28-
TokenType frontend.Variable `gnark:"public"`
29+
TypeCommitment frontend.Variable `gnark:"public"`
2930

3031
// Private inputs
31-
Value frontend.Variable
32-
Randomness frontend.Variable
33-
RCV frontend.Variable
32+
Value frontend.Variable
33+
TokenType frontend.Variable
34+
Randomness frontend.Variable
35+
RCV frontend.Variable
36+
TypeRandomness frontend.Variable
3437

3538
// Compile-time parameter
3639
// MaxBits is the bit-width of the value range constraint.
@@ -77,5 +80,18 @@ func (c *OutputCircuit) Define(api frontend.API) error {
7780
_ = api.ToBinary(c.Value, maxBits)
7881
api.AssertIsDifferent(c.Value, 0)
7982

83+
// ── Constraint Group 4: Type Commitment Integrity ───────────────────────
84+
// Enforce: TypeCommitment == MiMC(TokenType, TypeRandomness)
85+
//
86+
// The same TokenType private variable used in Group 1 (note commitment)
87+
// appears here, so the ZK proof structurally binds the committed type
88+
// to the type encoded in the note commitment. The validator only sees
89+
// TypeCommitment (a hiding commitment) — not the plaintext type.
90+
tc, err := gadgets.HashCircuit(api, c.TokenType, c.TypeRandomness)
91+
if err != nil {
92+
return err
93+
}
94+
api.AssertIsEqual(tc, c.TypeCommitment)
95+
8096
return nil
8197
}

0 commit comments

Comments
 (0)