Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AVM: new teal opcodes for the MiMC hash function to support Zero Knowledge Proofs #5978

Merged
merged 21 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions data/transactions/logic/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ dup; dup
falcon_verify
`

const mimcNonsense = `
pushbytes 0x11223344556677889900aabbccddeeff11223344556677889900aabbccddeeff
mimc BLS12_381Mp111
`

const v8Nonsense = v7Nonsense + switchNonsense + frameNonsense + matchNonsense + boxNonsense

const v9Nonsense = v8Nonsense
Expand All @@ -450,7 +455,7 @@ const spliceNonsence = `

const v10Nonsense = v9Nonsense + pairingNonsense + spliceNonsence

const v11Nonsense = v10Nonsense + incentiveNonsense + stateProofNonsense
const v11Nonsense = v10Nonsense + incentiveNonsense + stateProofNonsense + mimcNonsense

const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f2310231123122313231418191a1b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b400b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a"

Expand All @@ -475,8 +480,9 @@ const v10Compiled = v9Compiled + pairingCompiled + spliceCompiled
const incentiveCompiled = "757401"

const stateProofCompiled = "80070123456789abcd86494985"
const mimcCompiled = "802011223344556677889900aabbccddeeff11223344556677889900aabbccddeeffe601"

const V11Compiled = v10Compiled + incentiveCompiled + stateProofCompiled
const V11Compiled = v10Compiled + incentiveCompiled + stateProofCompiled + mimcCompiled

var nonsense = map[uint64]string{
1: v1Nonsense,
Expand Down Expand Up @@ -557,7 +563,7 @@ func TestAssemble(t *testing.T) {
}
}

var experiments = []uint64{spOpcodesVersion}
var experiments = []uint64{spOpcodesVersion, mimcVersion}

// TestExperimental forces a conscious choice to promote "experimental" opcode
// groups. This will fail when we increment vFuture's LogicSigVersion. If we had
Expand Down Expand Up @@ -3181,7 +3187,7 @@ func TestMacros(t *testing.T) {
#define ==? ==; bnz
pushint 1; pushint 2; ==? label1
err
label1:
label1:
pushint 1`,
)

Expand Down Expand Up @@ -3219,19 +3225,19 @@ func TestMacros(t *testing.T) {
pushbytes 0xddf2554d
txna ApplicationArgs 0
==
bnz kickstart
pushbytes 0x903f4535
bnz kickstart
pushbytes 0x903f4535
txna ApplicationArgs 0
==
bnz portal_transfer
bnz portal_transfer
kickstart:
pushint 1
portal_transfer:
pushint 1
`, `
#define abi-route txna ApplicationArgs 0; ==; bnz
method "kickstart(account)void"; abi-route kickstart
method "portal_transfer(byte[])byte[]"; abi-route portal_transfer
#define abi-route txna ApplicationArgs 0; ==; bnz
method "kickstart(account)void"; abi-route kickstart
method "portal_transfer(byte[])byte[]"; abi-route portal_transfer
kickstart:
pushint 1
portal_transfer:
Expand Down Expand Up @@ -3287,7 +3293,7 @@ add:
extract_uint32
stores

load 1; load 2; +
load 1; load 2; +
store 255

int 255
Expand All @@ -3311,11 +3317,11 @@ add:
#define abi-decode-uint32 ;int 0; extract_uint32;
#define abi-encode-uint32 ;itob;extract 4 0;

#define abi-encode-bytes ;dup; len; abi-encode-uint16; swap; concat;
#define abi-encode-bytes ;dup; len; abi-encode-uint16; swap; concat;
#define abi-decode-bytes ;extract 2 0;

// abi method handling
#define abi-route ;txna ApplicationArgs 0; ==; bnz
// abi method handling
#define abi-route ;txna ApplicationArgs 0; ==; bnz
#define abi-return ;pushbytes 0x151f7c75; swap; concat; log; int 1; return;

// stanza: "set $var from-{type}"
Expand Down Expand Up @@ -3344,15 +3350,15 @@ echo:


// add handler
method "add(uint32,uint32)uint32"; abi-route add
method "add(uint32,uint32)uint32"; abi-route add
add:
#define x 1
parse x from-uint32
parse x from-uint32

#define y 2
parse y from-uint32

#define sum 255
#define sum 255
load x; load y; +; store sum

returns sum as-uint32
Expand Down
44 changes: 44 additions & 0 deletions data/transactions/logic/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,59 @@
"crypto/sha512"
"errors"
"fmt"
"hash"
"math/big"

"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/crypto/secp256k1"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-sumhash"
"golang.org/x/crypto/sha3"

bls12_381mimc "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc"
bn254mimc "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc"
)

// mimc is implemented for compatibility with zk circuits,
// matching the implementation in circuits generated by gnark
func opMimc(cx *EvalContext) error {
config := MimcConfig(cx.program[cx.pc+1])
fs, ok := mimcConfigSpecByField(config)
if !ok { // no version check yet, all configs appeared at once
return fmt.Errorf("invalid mimc config %s", config)

Check warning on line 45 in data/transactions/logic/crypto.go

View check run for this annotation

Codecov / codecov/patch

data/transactions/logic/crypto.go#L45

Added line #L45 was not covered by tests
}

last := len(cx.Stack) - 1
data := cx.Stack[last].Bytes
if len(data) == 0 {
return fmt.Errorf("the input data cannot be empty")
}
if len(data)%32 != 0 {
return fmt.Errorf("the input data must be a multiple of 32 bytes")
}

var mimc hash.Hash

switch fs.field {
case BN254Mp110:
mimc = bn254mimc.NewMiMC()
case BLS12_381Mp111:
mimc = bls12_381mimc.NewMiMC()
default:
return fmt.Errorf("invalid mimc group %s", config)

Check warning on line 65 in data/transactions/logic/crypto.go

View check run for this annotation

Codecov / codecov/patch

data/transactions/logic/crypto.go#L64-L65

Added lines #L64 - L65 were not covered by tests
}

// unlike most hash.Hash objects, a mimc hasher has strict requirements. The
// input must be a multiple of the curve's encoded element size, and no
// element may exceed the corve modulus.
if _, err := mimc.Write(cx.Stack[last].Bytes); err != nil {
return fmt.Errorf("invalid mimc input %w", err)
}

cx.Stack[last].Bytes = mimc.Sum(nil)
giuliop marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

func opSHA256(cx *EvalContext) error {
last := len(cx.Stack) - 1
hash := sha256.Sum256(cx.Stack[last].Bytes)
Expand Down
75 changes: 74 additions & 1 deletion data/transactions/logic/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,76 @@ byte 0x98D2C31612EA500279B6753E5F6E780CA63EBA8274049664DAD66A2565ED1D2A
testAccepts(t, progText, 1)
}

func TestMimc(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

// Test vectors from https://github.com/giuliop/test-mimc-opcodes/blob/main/mimctest/main.go
// generated by instantiating a zk-circuit that processes MiMC hash of the preimages.
// We test success for 32-byte and 96-byte preimages, and failure for preimage input size of 0,
// input size not multiple of 32 bytes, and chunks representing values greater than the modulus.
giuliop marked this conversation as resolved.
Show resolved Hide resolved

type PreImageTestVector struct {
PreImage string
ShouldSucceed bool
}
preImageTestVectors := []PreImageTestVector{
{"0x",
false}, // zero-length input
{"0x23a950068dd3d1e21cee48e7919be7ae32cdef70311fc486336ea9d4b5042535",
true}, // 32 bytes, less than modulus
{"0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000002",
false}, // 32 bytes, more than modulus
{"0xdeadf00d",
false}, // less than 32 byte
{"0x183de351a72141d79c51a27d10405549c98302cb2536c5968deeb3cba635121723a950068dd3d1e21cee48e7919be7ae32cdef70311fc486336ea9d4b504253530644e72e131a029b85045b68181585d2833e84879b9709143e1f593ef676981",
true}, // 32 bytes, less than modulus | 32 bytes, less than modulus | 32 bytes, less than modulus
{"0x183de351a72141d79c51a27d10405549c98302cb2536c5968deeb3cba635121723a950068dd3d1e21cee48e7919be7ae32cdef70311fc486336ea9d4b504253573eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000002",
false}, // 32 bytes, less than modulus | 32 bytes, less than modulus | 32 bytes, more than modulus
{"0x183de351a72141d79c51a27d10405549c98302cb2536c5968deeb3cba635121723a950068dd3d1e21cee48e7919be7ae32cdef70311fc486336ea9d4b5042535abba",
false}, // 32 bytes, less than modulus | 32 bytes, less than modulus | less than 32 bytes
}

circuitHashTestVectors := map[string][]string{
"BN254Mp110": {
"20104241803663641422577121134203490505137011783614913652735802145961801733870",
"12886436712380113721405259596386800092738845035233065858332878701083870690753",
"19565877911319815535452130675266047290072088868113536892077808700068649624391",
"1037254799353855871006189384309576393135431139055333626960622147300727796413",
"6040222623731283351958201178122781676432899642144860863024149088913741383362",
"21691351735381703396517600859480938764038501053226864452091917666642352837076",
"10501393540371963307040960561318023073151272109639330842515119353134949995409",
},
"BLS12_381Mp111": {
"17991912493598890696181760734961918471863781118188078948205844982816313445306",
"8791766422525455185980675814845076441443662947059416063736889106252015893524",
"35137972692771717943992759113612269767581262500164574105059686144346651628747",
"15039173432183897369859775531867817848264266283034981501223857291379142522368",
"12964111614552580241101202600014316932811348627866250816177200046290462797607",
"21773894974440411325489312534417904228129169539217646609523079291104496302656",
"9873666029497961930790892458408217321483390383568592297687427911011295910871",
},
}

for _, config := range []string{"BN254Mp110", "BLS12_381Mp111"} {
for i, preImageTestVector := range preImageTestVectors {
var n big.Int
n.SetString(circuitHashTestVectors[config][i], 10)
circuitHash := n.Bytes()
progText := fmt.Sprintf(`byte %s
mimc %s

byte 0x%x
==`, preImageTestVector.PreImage, config, circuitHash)
if preImageTestVector.ShouldSucceed {
testAccepts(t, progText, 11)
} else {
testPanics(t, progText, 11)
}
}
}
}

// This is patterned off vrf_test.go, but we don't create proofs here, we only
// check that the output is correct, given the proof.
func testVrfApp(pubkey, proof, data string, output string) string {
Expand Down Expand Up @@ -713,8 +783,11 @@ int ` + fmt.Sprintf("%d", testLogicBudget-2500-8) + `
}

func BenchmarkHashes(b *testing.B) {
for _, hash := range []string{"sha256", "keccak256" /* skip, same as keccak "sha3_256", */, "sha512_256", "sumhash512"} {
for _, hash := range []string{"sha256", "keccak256" /* skip, same as keccak "sha3_256", */, "sha512_256", "sumhash512", "mimc BN254Mp110", "mimc BLS12_381Mp111"} {
for _, size := range []int{0, 32, 128, 512, 1024, 4096} {
if size == 0 && (hash == "mimc BN254Mp110" || hash == "mimc BLS12_381Mp111") {
continue
}
b.Run(hash+"-"+strconv.Itoa(size), func(b *testing.B) {
benchmarkOperation(b, "", fmt.Sprintf("int %d; bzero; %s; pop", size, hash), "int 1")
})
Expand Down
9 changes: 8 additions & 1 deletion data/transactions/logic/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ var opDescByName = map[string]OpDesc{
"sumhash512": {"sumhash512 of value A, yields [64]byte", "", nil},
"falcon_verify": {"for (data A, compressed-format signature B, pubkey C) verify the signature of data against the pubkey", "", nil},

"mimc": {"MIMC hash of value A in the curve field specified by configuration C, yields [32]byte", "" +
"A is split into 32-byte chunks and written to the hasher to finally compute the MiMC hash. Fail if A's length is not a multiple of 32 or any chunk encodes a value greater than the curve modulus.\n\n" +
"Note that the MIMC hash function has known collisions since any input which is a multiple of the elliptic curve modulus it uses will hash to the same value.\n" +
"MIMC is thus not a general purpose hash function, but meant to be used in zero knowledge applications to match a zk-circuit implementation.",
[]string{"configuration index"},
},

"ed25519verify": {"for (data A, signature B, pubkey C) verify the signature of (\"ProgData\" || program_hash || data) against the pubkey => {0 or 1}", "The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack.", nil},
"ed25519verify_bare": {"for (data A, signature B, pubkey C) verify the signature of the data against the pubkey => {0 or 1}", "", nil},
"ecdsa_verify": {"for (data A, signature B, C and pubkey D, E) verify the signature of the data against the pubkey => {0 or 1}", "The 32 byte Y-component of a public key is the last element on the stack, preceded by X-component of a pubkey, preceded by S and R components of a signature, preceded by the data that is fifth element on the stack. All values are big-endian encoded. The signed data must be 32 bytes long, and signatures in lower-S form are only accepted.", []string{"curve index"}},
Expand Down Expand Up @@ -354,7 +361,7 @@ var OpGroups = map[string][]string{
"Byte Array Manipulation": {"getbit", "setbit", "getbyte", "setbyte", "concat", "len", "substring", "substring3", "extract", "extract3", "extract_uint16", "extract_uint32", "extract_uint64", "replace2", "replace3", "base64_decode", "json_ref"},
"Byte Array Arithmetic": {"b+", "b-", "b/", "b*", "b<", "b>", "b<=", "b>=", "b==", "b!=", "b%", "bsqrt"},
"Byte Array Logic": {"b|", "b&", "b^", "b~"},
"Cryptography": {"sha256", "keccak256", "sha512_256", "sha3_256", "sumhash512", "falcon_verify", "ed25519verify", "ed25519verify_bare", "ecdsa_verify", "ecdsa_pk_recover", "ecdsa_pk_decompress", "vrf_verify", "ec_add", "ec_scalar_mul", "ec_pairing_check", "ec_multi_scalar_mul", "ec_subgroup_check", "ec_map_to"},
"Cryptography": {"sha256", "keccak256", "sha512_256", "sha3_256", "sumhash512", "falcon_verify", "ed25519verify", "ed25519verify_bare", "ecdsa_verify", "ecdsa_pk_recover", "ecdsa_pk_decompress", "vrf_verify", "ec_add", "ec_scalar_mul", "ec_pairing_check", "ec_multi_scalar_mul", "ec_subgroup_check", "ec_map_to", "mimc"},
"Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "pushints", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "pushbytess", "bzero", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "args", "txn", "gtxn", "txna", "txnas", "gtxna", "gtxnas", "gtxns", "gtxnsa", "gtxnsas", "global", "load", "loads", "store", "stores", "gload", "gloads", "gloadss", "gaid", "gaids"},
"Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "popn", "dup", "dup2", "dupn", "dig", "bury", "cover", "uncover", "frame_dig", "frame_bury", "swap", "select", "assert", "callsub", "proto", "retsub", "switch", "match"},
"State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get", "acct_params_get", "voter_params_get", "online_stake", "log", "block"},
Expand Down
3 changes: 3 additions & 0 deletions data/transactions/logic/evalStateful_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3254,6 +3254,9 @@ func TestReturnTypes(t *testing.T) {

"box_create": "int 9; +; box_create", // make the size match the 10 in CreateBox
"box_put": "byte 0x010203040506; concat; box_put", // make the 4 byte arg into a 10

// mimc requires an input size multiple of 32 bytes.
"mimc": ": byte 0x0000000000000000000000000000000000000000000000000000000000000001; mimc BN254Mp110",
}

/* Make sure the specialCmd tests the opcode in question */
Expand Down
71 changes: 70 additions & 1 deletion data/transactions/logic/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"github.com/algorand/go-algorand/protocol"
)

//go:generate stringer -type=TxnField,GlobalField,AssetParamsField,AppParamsField,AcctParamsField,AssetHoldingField,OnCompletionConstType,EcdsaCurve,EcGroup,Base64Encoding,JSONRefType,VoterParamsField,VrfStandard,BlockField -output=fields_string.go
//go:generate stringer -type=TxnField,GlobalField,AssetParamsField,AppParamsField,AcctParamsField,AssetHoldingField,OnCompletionConstType,EcdsaCurve,EcGroup,MimcConfig,Base64Encoding,JSONRefType,VoterParamsField,VrfStandard,BlockField -output=fields_string.go

// FieldSpec unifies the various specs for assembly, disassembly, and doc generation.
type FieldSpec interface {
Expand Down Expand Up @@ -785,6 +785,68 @@
ecGroupSpecByName,
}

// MimcConfig is an enum for the `mimc` opcode
type MimcConfig int

const (
// BN254Mp110 is the default MiMC configuration for the BN254 curve with Miyaguchi-Preneel mode, 110 rounds, exponent 5, seed "seed"
BN254Mp110 MimcConfig = iota
// BLS12_381Mp111 is the default MiMC configuration for the BLS12-381 curve with Miyaguchi-Preneel mode, 111 rounds, exponent 5, seed "seed"
BLS12_381Mp111
invalidMimcConfig // compile-time constant for number of fields
)

var mimcConfigNames [invalidMimcConfig]string

type mimcConfigSpec struct {
field MimcConfig
doc string
}

func (fs mimcConfigSpec) Field() byte {
return byte(fs.field)
}
func (fs mimcConfigSpec) Type() StackType {
return StackNone // Will not show, since all are untyped
}
func (fs mimcConfigSpec) OpVersion() uint64 {
return mimcVersion

Check warning on line 813 in data/transactions/logic/fields.go

View check run for this annotation

Codecov / codecov/patch

data/transactions/logic/fields.go#L812-L813

Added lines #L812 - L813 were not covered by tests
}
func (fs mimcConfigSpec) Version() uint64 {
return mimcVersion
}
func (fs mimcConfigSpec) Note() string {
return fs.doc

Check warning on line 819 in data/transactions/logic/fields.go

View check run for this annotation

Codecov / codecov/patch

data/transactions/logic/fields.go#L818-L819

Added lines #L818 - L819 were not covered by tests
}

var mimcConfigSpecs = [...]mimcConfigSpec{
{BN254Mp110, "MiMC configuration for the BN254 curve with Miyaguchi-Preneel mode, 110 rounds, exponent 5, seed \"seed\""},
{BLS12_381Mp111, "MiMC configuration for the BLS12-381 curve with Miyaguchi-Preneel mode, 111 rounds, exponent 5, seed \"seed\""},
}

func mimcConfigSpecByField(c MimcConfig) (mimcConfigSpec, bool) {
if int(c) >= len(mimcConfigSpecs) {
return mimcConfigSpec{}, false

Check warning on line 829 in data/transactions/logic/fields.go

View check run for this annotation

Codecov / codecov/patch

data/transactions/logic/fields.go#L829

Added line #L829 was not covered by tests
}
return mimcConfigSpecs[c], true
}

var mimcConfigSpecByName = make(mimcConfigNameSpecMap, len(mimcConfigNames))

type mimcConfigNameSpecMap map[string]mimcConfigSpec

func (s mimcConfigNameSpecMap) get(name string) (FieldSpec, bool) {
fs, ok := s[name]
return fs, ok
}

// MimcConfigs collects details about the constants used to describe MimcConfigs
var MimcConfigs = FieldGroup{
"Mimc Configurations", "Parameters",
mimcConfigNames[:],
mimcConfigSpecByName,
}

// Base64Encoding is an enum for the `base64decode` opcode
type Base64Encoding int

Expand Down Expand Up @@ -1548,6 +1610,13 @@
ecGroupSpecByName[s.field.String()] = s
}

equal(len(mimcConfigSpecs), len(mimcConfigNames))
for i, s := range mimcConfigSpecs {
equal(int(s.field), i)
mimcConfigNames[s.field] = s.field.String()
mimcConfigSpecByName[s.field.String()] = s
}

equal(len(base64EncodingSpecs), len(base64EncodingNames))
for i, s := range base64EncodingSpecs {
equal(int(s.field), i)
Expand Down
Loading