From 74725fa29be5a0baf5f26d11a1257a8224289ccb Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Tue, 14 May 2024 18:07:40 +0530 Subject: [PATCH 01/10] Implement basic SquashDict --- pkg/hintrunner/zero/hintcode.go | 1 + pkg/hintrunner/zero/zerohint.go | 2 + pkg/hintrunner/zero/zerohint_dictionaries.go | 140 +++++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/pkg/hintrunner/zero/hintcode.go b/pkg/hintrunner/zero/hintcode.go index d66f5bbc..6ad80d79 100644 --- a/pkg/hintrunner/zero/hintcode.go +++ b/pkg/hintrunner/zero/hintcode.go @@ -109,6 +109,7 @@ const ( keccakWriteArgs string = "segments.write_arg(ids.inputs, [ids.low % 2 ** 64, ids.low // 2 ** 64])\nsegments.write_arg(ids.inputs + 2, [ids.high % 2 ** 64, ids.high // 2 ** 64])" // ------ Dictionaries hints related code ------ + squashDictCode string = "dict_access_size = ids.DictAccess.SIZE\naddress = ids.dict_accesses.address_\nassert ids.ptr_diff % dict_access_size == 0, \\\n 'Accesses array size must be divisible by DictAccess.SIZE'\nn_accesses = ids.n_accesses\nif '__squash_dict_max_size' in globals():\n assert n_accesses <= __squash_dict_max_size, \\\n f'squash_dict() can only be used with n_accesses<={__squash_dict_max_size}. ' \\\n f'Got: n_accesses={n_accesses}.'\n# A map from key to the list of indices accessing it.\naccess_indices = {}\nfor i in range(n_accesses):\n key = memory[address + dict_access_size * i]\n access_indices.setdefault(key, []).append(i)\n# Descending list of keys.\nkeys = sorted(access_indices.keys(), reverse=True)\n# Are the keys used bigger than range_check bound.\nids.big_keys = 1 if keys[0] >= range_check_builtin.bound else 0\nids.first_key = key = keys.pop()" squashDictInnerAssertLenKeys string = "assert len(keys) == 0" squashDictInnerLenAssert string = "assert len(current_access_indices) == 0" squashDictInnerNextKey string = "assert len(keys) > 0, 'No keys left but remaining_accesses > 0.'\nids.next_key = key = keys.pop()" diff --git a/pkg/hintrunner/zero/zerohint.go b/pkg/hintrunner/zero/zerohint.go index 03cbd72d..d8ccbee1 100644 --- a/pkg/hintrunner/zero/zerohint.go +++ b/pkg/hintrunner/zero/zerohint.go @@ -159,6 +159,8 @@ func GetHintFromCode(program *zero.ZeroProgram, rawHint zero.Hint, hintPC uint64 case usortBodyCode: return createUsortBodyHinter(resolver) // Dictionaries hints + case squashDictCode: + return createSquashDictHinter(resolver) case squashDictInnerAssertLenKeys: return createSquashDictInnerAssertLenKeysHinter() case squashDictInnerLenAssert: diff --git a/pkg/hintrunner/zero/zerohint_dictionaries.go b/pkg/hintrunner/zero/zerohint_dictionaries.go index f1930b5e..266dcaf3 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries.go @@ -2,6 +2,7 @@ package zero import ( "fmt" + "sort" "github.com/NethermindEth/cairo-vm-go/pkg/hintrunner/hinter" "github.com/NethermindEth/cairo-vm-go/pkg/utils" @@ -10,6 +11,145 @@ import ( f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp" ) +func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinter.ResOperander) hinter.Hinter { + return &GenericZeroHinter{ + Name: "SquashDict", + Op: func(vm *VM.VirtualMachine, ctx *hinter.HintRunnerContext) error { + //> dict_access_size = ids.DictAccess.SIZE + //> address = ids.dict_accesses.address_ + //> assert ids.ptr_diff % dict_access_size == 0, \ + //> 'Accesses array size must be divisible by DictAccess.SIZE' + //> n_accesses = ids.n_accesses + //> if '__squash_dict_max_size' in globals(): + //> assert n_accesses <= __squash_dict_max_size, \ + //> f'squash_dict() can only be used with n_accesses<={__squash_dict_max_size}. ' \ + //> f'Got: n_accesses={n_accesses}.' + //> # A map from key to the list of indices accessing it. + //> access_indices = {} + //> for i in range(n_accesses): + //> key = memory[address + dict_access_size * i] + //> access_indices.setdefault(key, []).append(i) + //> # Descending list of keys. + //> keys = sorted(access_indices.keys(), reverse=True) + //> # Are the keys used bigger than range_check bound. + //> ids.big_keys = 1 if keys[0] >= range_check_builtin.bound else 0 + //> ids.first_key = key = keys.pop() + + //> dict_access_size = ids.DictAccess.SIZE + dictAccessSize := uint64(3) + + //> address = ids.dict_accesses.address_ + address, err := dictAccesses.GetAddress(vm) + if err != nil { + return err + } + + //> assert ids.ptr_diff % dict_access_size == 0, \ + //> 'Accesses array size must be divisible by DictAccess.SIZE' + ptrDiffValue, err := hinter.ResolveAsUint64(vm, ptrDiff) + if err != nil { + return err + } + if ptrDiffValue%dictAccessSize != 0 { + return fmt.Errorf("Accesses array size must be divisible by DictAccess.SIZE") + } + + //> n_accesses = ids.n_accesses + nAccessesValue, err := hinter.ResolveAsUint64(vm, nAccesses) + if err != nil { + return err + } + + //> if '__squash_dict_max_size' in globals(): + //> assert n_accesses <= __squash_dict_max_size, \ + //> f'squash_dict() can only be used with n_accesses<={__squash_dict_max_size}. ' \ + //> f'Got: n_accesses={n_accesses}.' + // looks like __squash_dict_max_size is generally set as 2**20, + squashDictMaxSize := uint64(1048576) + if nAccessesValue > squashDictMaxSize { + return fmt.Errorf("squash_dict() can only be used with n_accesses<={%d}. Got: n_accesses={%d}.", squashDictMaxSize, nAccessesValue) + } + + //> # A map from key to the list of indices accessing it. + //> access_indices = {} + //> for i in range(n_accesses): + //> key = memory[address + dict_access_size * i] + //> access_indices.setdefault(key, []).append(i) + var keys []f.Element + accessIndices := make(map[f.Element][]uint64) + for i := uint64(0); i < nAccessesValue; i++ { + memoryAddress, err := address.AddOffset(int16(dictAccessSize * i)) + if err != nil { + return err + } + key, err := vm.Memory.ReadFromAddressAsElement(&memoryAddress) + if err != nil { + return err + } + accessIndices[key] = append(accessIndices[key], i) + keys = append(keys, key) + } + + //> # Descending list of keys. + //> keys = sorted(access_indices.keys(), reverse=True) + sort.Slice(keys, func(i, j int) bool { + return keys[i].Cmp(&keys[j]) > 0 + }) + + //> # Are the keys used bigger than range_check bound. + // TODO: won't keys[0] error out if ids.n_accesses = 0 ? + // TODO: won't value of keys always be within a felt? + //> ids.big_keys = 1 if keys[0] >= range_check_builtin.bound else 0 + bigKeysAddr, err := bigKeys.GetAddress(vm) + if err != nil { + return err + } + feltZeroMv := memory.MemoryValueFromFieldElement(&utils.FeltZero) + err = vm.Memory.WriteToAddress(&bigKeysAddr, &feltZeroMv) + if err != nil { + return err + } + + //> ids.first_key = key = keys.pop() + firstKeyAddr, err := firstKey.GetAddress(vm) + if err != nil { + return err + } + poppedKey := keys[len(keys)-1] + poppedKeyMv := memory.MemoryValueFromFieldElement(&poppedKey) + keys = keys[:len(keys)-1] + return vm.Memory.WriteToAddress(&firstKeyAddr, &poppedKeyMv) + + // TODO: should accessIndices, keys, key be stored in scope? + }, + } +} + +func createSquashDictHinter(resolver hintReferenceResolver) (hinter.Hinter, error) { + dictAccesses, err := resolver.GetResOperander("dict_accesses") + if err != nil { + return nil, err + } + ptrDiff, err := resolver.GetResOperander("ptr_diff") + if err != nil { + return nil, err + } + nAccesses, err := resolver.GetResOperander("n_accesses") + if err != nil { + return nil, err + } + bigKeys, err := resolver.GetResOperander("big_keys") + if err != nil { + return nil, err + } + firstKey, err := resolver.GetResOperander("first_key") + if err != nil { + return nil, err + } + + return newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey), nil +} + // SquashDictInnerAssertLenKeys hint asserts that the length // of the `keys` descending list is zero during the squashing process // From bafa5c458088d3c3551d673e8364940ac4afc823 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 17:37:07 +0530 Subject: [PATCH 02/10] Add more functionality --- pkg/hintrunner/zero/zerohint_dictionaries.go | 36 ++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries.go b/pkg/hintrunner/zero/zerohint_dictionaries.go index 266dcaf3..8dd1c86e 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries.go @@ -9,6 +9,7 @@ import ( VM "github.com/NethermindEth/cairo-vm-go/pkg/vm" "github.com/NethermindEth/cairo-vm-go/pkg/vm/memory" f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp" + "golang.org/x/exp/maps" ) func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinter.ResOperander) hinter.Hinter { @@ -64,7 +65,7 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte //> assert n_accesses <= __squash_dict_max_size, \ //> f'squash_dict() can only be used with n_accesses<={__squash_dict_max_size}. ' \ //> f'Got: n_accesses={n_accesses}.' - // looks like __squash_dict_max_size is generally set as 2**20, + // __squash_dict_max_size is always in scope and has a value of 2**20, squashDictMaxSize := uint64(1048576) if nAccessesValue > squashDictMaxSize { return fmt.Errorf("squash_dict() can only be used with n_accesses<={%d}. Got: n_accesses={%d}.", squashDictMaxSize, nAccessesValue) @@ -75,7 +76,6 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte //> for i in range(n_accesses): //> key = memory[address + dict_access_size * i] //> access_indices.setdefault(key, []).append(i) - var keys []f.Element accessIndices := make(map[f.Element][]uint64) for i := uint64(0); i < nAccessesValue; i++ { memoryAddress, err := address.AddOffset(int16(dictAccessSize * i)) @@ -87,25 +87,30 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte return err } accessIndices[key] = append(accessIndices[key], i) - keys = append(keys, key) } //> # Descending list of keys. //> keys = sorted(access_indices.keys(), reverse=True) + keys := maps.Keys(accessIndices) + if len(keys) == 0 { + return fmt.Errorf("empty keys array") + } sort.Slice(keys, func(i, j int) bool { return keys[i].Cmp(&keys[j]) > 0 }) - //> # Are the keys used bigger than range_check bound. - // TODO: won't keys[0] error out if ids.n_accesses = 0 ? - // TODO: won't value of keys always be within a felt? //> ids.big_keys = 1 if keys[0] >= range_check_builtin.bound else 0 bigKeysAddr, err := bigKeys.GetAddress(vm) if err != nil { return err } - feltZeroMv := memory.MemoryValueFromFieldElement(&utils.FeltZero) - err = vm.Memory.WriteToAddress(&bigKeysAddr, &feltZeroMv) + var bigKeysMv memory.MemoryValue + if utils.FeltIsPositive(&keys[0]) { + bigKeysMv = memory.MemoryValueFromFieldElement(&utils.FeltZero) + } else { + bigKeysMv = memory.MemoryValueFromFieldElement(&utils.FeltOne) + } + err = vm.Memory.WriteToAddress(&bigKeysAddr, &bigKeysMv) if err != nil { return err } @@ -118,9 +123,20 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte poppedKey := keys[len(keys)-1] poppedKeyMv := memory.MemoryValueFromFieldElement(&poppedKey) keys = keys[:len(keys)-1] - return vm.Memory.WriteToAddress(&firstKeyAddr, &poppedKeyMv) + err = vm.Memory.WriteToAddress(&firstKeyAddr, &poppedKeyMv) + if err != nil { + return err + } - // TODO: should accessIndices, keys, key be stored in scope? + err = ctx.ScopeManager.AssignVariable("access_indices", accessIndices) + if err != nil { + return err + } + err = ctx.ScopeManager.AssignVariable("keys", keys) + if err != nil { + return err + } + return ctx.ScopeManager.AssignVariable("key", poppedKey) }, } } From 12748c1d3003b5207ff21abf8ff6404f38b93c73 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 18:53:41 +0530 Subject: [PATCH 03/10] Add error tests --- .../zero/zerohint_dictionaries_test.go | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries_test.go b/pkg/hintrunner/zero/zerohint_dictionaries_test.go index 34e10f23..59843992 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries_test.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries_test.go @@ -118,5 +118,79 @@ func TestZeroHintDictionaries(t *testing.T) { }, }, }, + "SquashDict": { + { + operanders: []*hintOperander{ + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(7)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(4)}, + {Name: "big_keys", Kind: uninitialized}, + {Name: "first_key", Kind: uninitialized}, + }, + makeHinter: func(ctx *hintTestContext) hinter.Hinter { + return newSquashDictHint( + ctx.operanders["dict_accesses.1.key"], + ctx.operanders["ptr_diff"], + ctx.operanders["n_accesses"], + ctx.operanders["big_keys"], + ctx.operanders["first_key"], + ) + }, + errCheck: errorTextContains("Accesses array size must be divisible by DictAccess.SIZE"), + }, + { + operanders: []*hintOperander{ + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(6)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(1048577)}, + {Name: "big_keys", Kind: uninitialized}, + {Name: "first_key", Kind: uninitialized}, + }, + makeHinter: func(ctx *hintTestContext) hinter.Hinter { + return newSquashDictHint( + ctx.operanders["dict_accesses.1.key"], + ctx.operanders["ptr_diff"], + ctx.operanders["n_accesses"], + ctx.operanders["big_keys"], + ctx.operanders["first_key"], + ) + }, + errCheck: errorTextContains("squash_dict() can only be used with n_accesses<={1048576}. Got: n_accesses={1048577}."), + }, + { + operanders: []*hintOperander{ + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(6)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(0)}, + {Name: "big_keys", Kind: uninitialized}, + {Name: "first_key", Kind: uninitialized}, + }, + makeHinter: func(ctx *hintTestContext) hinter.Hinter { + return newSquashDictHint( + ctx.operanders["dict_accesses.1.key"], + ctx.operanders["ptr_diff"], + ctx.operanders["n_accesses"], + ctx.operanders["big_keys"], + ctx.operanders["first_key"], + ) + }, + errCheck: errorTextContains("empty keys array"), + }, + }, }) } From caa2aae3bc26317c3ea27fc4904af5dfa8f7fee0 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 19:41:44 +0530 Subject: [PATCH 04/10] Add a successful test --- pkg/hintrunner/zero/zerohint_dictionaries.go | 8 ++-- .../zero/zerohint_dictionaries_test.go | 43 +++++++++++++++---- pkg/hintrunner/zero/zerohint_utils_test.go | 8 ++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries.go b/pkg/hintrunner/zero/zerohint_dictionaries.go index 8dd1c86e..fc6927e1 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries.go @@ -120,10 +120,10 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte if err != nil { return err } - poppedKey := keys[len(keys)-1] - poppedKeyMv := memory.MemoryValueFromFieldElement(&poppedKey) + firstKeyValue := keys[len(keys)-1] + firstKeyMv := memory.MemoryValueFromFieldElement(&firstKeyValue) keys = keys[:len(keys)-1] - err = vm.Memory.WriteToAddress(&firstKeyAddr, &poppedKeyMv) + err = vm.Memory.WriteToAddress(&firstKeyAddr, &firstKeyMv) if err != nil { return err } @@ -136,7 +136,7 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte if err != nil { return err } - return ctx.ScopeManager.AssignVariable("key", poppedKey) + return ctx.ScopeManager.AssignVariable("key", firstKeyValue) }, } } diff --git a/pkg/hintrunner/zero/zerohint_dictionaries_test.go b/pkg/hintrunner/zero/zerohint_dictionaries_test.go index 59843992..aff263d4 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries_test.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries_test.go @@ -169,14 +169,23 @@ func TestZeroHintDictionaries(t *testing.T) { }, { operanders: []*hintOperander{ - {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(10)}, - {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(10)}, - {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(10)}, - {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(10)}, - {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, - {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, - {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(6)}, - {Name: "n_accesses", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(8)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(1)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.key", Kind: apRelative, Value: feltUint64(21)}, + {Name: "dict_accesses.3.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.4.key", Kind: apRelative, Value: feltUint64(22)}, + {Name: "dict_accesses.4.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.4.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.key", Kind: apRelative, Value: feltUint64(6)}, + {Name: "dict_accesses.5.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(15)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(5)}, {Name: "big_keys", Kind: uninitialized}, {Name: "first_key", Kind: uninitialized}, }, @@ -189,7 +198,23 @@ func TestZeroHintDictionaries(t *testing.T) { ctx.operanders["first_key"], ) }, - errCheck: errorTextContains("empty keys array"), + check: func(t *testing.T, ctx *hintTestContext) { + allVarValueEquals(map[string]*fp.Element{ + "big_keys": feltInt64(0), + "first_key": feltInt64(1), + })(t, ctx) + allVarValueInScopeEquals(map[string]any{ + "access_indices": map[fp.Element][]uint64{ + *feltUint64(8): {0}, + *feltUint64(1): {1}, + *feltUint64(21): {2}, + *feltUint64(22): {3}, + *feltUint64(6): {4}, + }, + "keys": []fp.Element{*feltUint64(22), *feltUint64(21), *feltUint64(8), *feltUint64(6)}, + "key": *feltUint64(1), + })(t, ctx) + }, }, }, }) diff --git a/pkg/hintrunner/zero/zerohint_utils_test.go b/pkg/hintrunner/zero/zerohint_utils_test.go index 6575e2d0..ba8daca3 100644 --- a/pkg/hintrunner/zero/zerohint_utils_test.go +++ b/pkg/hintrunner/zero/zerohint_utils_test.go @@ -187,6 +187,14 @@ func varValueInScopeEquals(varName string, expected any) func(t *testing.T, ctx t.Fatalf("%s scope value mismatch:\nhave: %v\nwant: %v", varName, value, expected) } } + case map[fp.Element][]uint64: + { + value := value.(map[fp.Element][]uint64) + expected := expected.(map[fp.Element][]uint64) + if !reflect.DeepEqual(value, expected) { + t.Fatalf("%s scope value mismatch:\nhave: %v\nwant: %v", varName, value, expected) + } + } default: { if value != expected { From 4fe1becd55a61ac53088a40a099b7e88d9ce2fd2 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 19:55:39 +0530 Subject: [PATCH 05/10] Test more successful scenarios --- .../zero/zerohint_dictionaries_test.go | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries_test.go b/pkg/hintrunner/zero/zerohint_dictionaries_test.go index aff263d4..b8dc3a56 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries_test.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/NethermindEth/cairo-vm-go/pkg/hintrunner/hinter" + "github.com/NethermindEth/cairo-vm-go/pkg/utils" "github.com/consensys/gnark-crypto/ecc/stark-curve/fp" ) @@ -127,6 +128,7 @@ func TestZeroHintDictionaries(t *testing.T) { {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(10)}, {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, + // checking if it catches ptr_diff % 3 != 0 {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(7)}, {Name: "n_accesses", Kind: apRelative, Value: feltUint64(4)}, {Name: "big_keys", Kind: uninitialized}, @@ -152,6 +154,7 @@ func TestZeroHintDictionaries(t *testing.T) { {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(10)}, {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(10)}, {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(6)}, + // checking if it catches n_accesses > 1048576 {Name: "n_accesses", Kind: apRelative, Value: feltUint64(1048577)}, {Name: "big_keys", Kind: uninitialized}, {Name: "first_key", Kind: uninitialized}, @@ -169,6 +172,7 @@ func TestZeroHintDictionaries(t *testing.T) { }, { operanders: []*hintOperander{ + // random correct values {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(8)}, {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(0)}, @@ -178,6 +182,7 @@ func TestZeroHintDictionaries(t *testing.T) { {Name: "dict_accesses.3.key", Kind: apRelative, Value: feltUint64(21)}, {Name: "dict_accesses.3.prev_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.3.new_value", Kind: apRelative, Value: feltUint64(0)}, + // largest key within range_check_builtin.bound {Name: "dict_accesses.4.key", Kind: apRelative, Value: feltUint64(22)}, {Name: "dict_accesses.4.prev_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.4.new_value", Kind: apRelative, Value: feltUint64(0)}, @@ -216,6 +221,107 @@ func TestZeroHintDictionaries(t *testing.T) { })(t, ctx) }, }, + { + operanders: []*hintOperander{ + // random correct values + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(8)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(1)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.key", Kind: apRelative, Value: feltUint64(21)}, + {Name: "dict_accesses.3.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.new_value", Kind: apRelative, Value: feltUint64(0)}, + // largest key bigger than range_check_builtin.bound + {Name: "dict_accesses.4.key", Kind: apRelative, Value: &utils.FeltMax128}, + {Name: "dict_accesses.4.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.4.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.key", Kind: apRelative, Value: feltUint64(6)}, + {Name: "dict_accesses.5.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(15)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(5)}, + {Name: "big_keys", Kind: uninitialized}, + {Name: "first_key", Kind: uninitialized}, + }, + makeHinter: func(ctx *hintTestContext) hinter.Hinter { + return newSquashDictHint( + ctx.operanders["dict_accesses.1.key"], + ctx.operanders["ptr_diff"], + ctx.operanders["n_accesses"], + ctx.operanders["big_keys"], + ctx.operanders["first_key"], + ) + }, + check: func(t *testing.T, ctx *hintTestContext) { + allVarValueEquals(map[string]*fp.Element{ + "big_keys": feltInt64(1), + "first_key": feltInt64(1), + })(t, ctx) + allVarValueInScopeEquals(map[string]any{ + "access_indices": map[fp.Element][]uint64{ + *feltUint64(8): {0}, + *feltUint64(1): {1}, + *feltUint64(21): {2}, + utils.FeltMax128: {3}, + *feltUint64(6): {4}, + }, + "keys": []fp.Element{utils.FeltMax128, *feltUint64(21), *feltUint64(8), *feltUint64(6)}, + "key": *feltUint64(1), + })(t, ctx) + }, + }, + { + operanders: []*hintOperander{ + // random correct values where keys are repeated + {Name: "dict_accesses.1.key", Kind: apRelative, Value: feltUint64(80)}, + {Name: "dict_accesses.1.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.1.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.key", Kind: apRelative, Value: feltUint64(29)}, + {Name: "dict_accesses.2.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.2.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.key", Kind: apRelative, Value: feltUint64(210)}, + {Name: "dict_accesses.3.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.3.new_value", Kind: apRelative, Value: feltUint64(0)}, + // largest key bigger than range_check_builtin.bound + {Name: "dict_accesses.4.key", Kind: apRelative, Value: &utils.FeltMax128}, + {Name: "dict_accesses.4.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.4.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.key", Kind: apRelative, Value: feltUint64(29)}, + {Name: "dict_accesses.5.prev_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "dict_accesses.5.new_value", Kind: apRelative, Value: feltUint64(0)}, + {Name: "ptr_diff", Kind: apRelative, Value: feltUint64(15)}, + {Name: "n_accesses", Kind: apRelative, Value: feltUint64(5)}, + {Name: "big_keys", Kind: uninitialized}, + {Name: "first_key", Kind: uninitialized}, + }, + makeHinter: func(ctx *hintTestContext) hinter.Hinter { + return newSquashDictHint( + ctx.operanders["dict_accesses.1.key"], + ctx.operanders["ptr_diff"], + ctx.operanders["n_accesses"], + ctx.operanders["big_keys"], + ctx.operanders["first_key"], + ) + }, + check: func(t *testing.T, ctx *hintTestContext) { + allVarValueEquals(map[string]*fp.Element{ + "big_keys": feltInt64(1), + "first_key": feltInt64(29), + })(t, ctx) + allVarValueInScopeEquals(map[string]any{ + "access_indices": map[fp.Element][]uint64{ + *feltUint64(80): {0}, + *feltUint64(29): {1, 4}, + *feltUint64(210): {2}, + utils.FeltMax128: {3}, + }, + "keys": []fp.Element{utils.FeltMax128, *feltUint64(210), *feltUint64(80)}, + "key": *feltUint64(29), + })(t, ctx) + }, + }, }, }) } From 784037689834823e90d7ffa3293cd155dde2c888 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 19:57:47 +0530 Subject: [PATCH 06/10] Test with more diverse values --- pkg/hintrunner/zero/zerohint_dictionaries_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries_test.go b/pkg/hintrunner/zero/zerohint_dictionaries_test.go index b8dc3a56..18491e90 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries_test.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries_test.go @@ -285,7 +285,7 @@ func TestZeroHintDictionaries(t *testing.T) { {Name: "dict_accesses.3.prev_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.3.new_value", Kind: apRelative, Value: feltUint64(0)}, // largest key bigger than range_check_builtin.bound - {Name: "dict_accesses.4.key", Kind: apRelative, Value: &utils.FeltMax128}, + {Name: "dict_accesses.4.key", Kind: apRelative, Value: &utils.FeltUpperBound}, {Name: "dict_accesses.4.prev_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.4.new_value", Kind: apRelative, Value: feltUint64(0)}, {Name: "dict_accesses.5.key", Kind: apRelative, Value: feltUint64(29)}, @@ -312,12 +312,12 @@ func TestZeroHintDictionaries(t *testing.T) { })(t, ctx) allVarValueInScopeEquals(map[string]any{ "access_indices": map[fp.Element][]uint64{ - *feltUint64(80): {0}, - *feltUint64(29): {1, 4}, - *feltUint64(210): {2}, - utils.FeltMax128: {3}, + *feltUint64(80): {0}, + *feltUint64(29): {1, 4}, + *feltUint64(210): {2}, + utils.FeltUpperBound: {3}, }, - "keys": []fp.Element{utils.FeltMax128, *feltUint64(210), *feltUint64(80)}, + "keys": []fp.Element{utils.FeltUpperBound, *feltUint64(210), *feltUint64(80)}, "key": *feltUint64(29), })(t, ctx) }, From e8faf6769bdf3ed01549b19471e3436178d99162 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Wed, 15 May 2024 20:03:53 +0530 Subject: [PATCH 07/10] Add integration test from lambda --- .../cairo_files/squash_dict.small.cairo | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 integration_tests/cairo_files/squash_dict.small.cairo diff --git a/integration_tests/cairo_files/squash_dict.small.cairo b/integration_tests/cairo_files/squash_dict.small.cairo new file mode 100644 index 00000000..7af92702 --- /dev/null +++ b/integration_tests/cairo_files/squash_dict.small.cairo @@ -0,0 +1,33 @@ +%builtins range_check + +from starkware.cairo.common.squash_dict import squash_dict +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.dict_access import DictAccess + +func main{range_check_ptr}() -> () { + alloc_locals; + let (dict_start: DictAccess*) = alloc(); + assert dict_start[0] = DictAccess(key=0, prev_value=100, new_value=100); + assert dict_start[1] = DictAccess(key=1, prev_value=50, new_value=50); + assert dict_start[2] = DictAccess(key=0, prev_value=100, new_value=200); + assert dict_start[3] = DictAccess(key=1, prev_value=50, new_value=100); + assert dict_start[4] = DictAccess(key=0, prev_value=200, new_value=300); + assert dict_start[5] = DictAccess(key=1, prev_value=100, new_value=150); + + let dict_end = dict_start + 6 * DictAccess.SIZE; + // (dict_start, dict_end) now represents the dictionary + // {0: 100, 1: 50, 0: 200, 1: 100, 0: 300, 1: 150}. + + // Squash the dictionary from an array of 6 DictAccess structs + // to an array of 2, with a single DictAccess entry per key. + let (local squashed_dict_start: DictAccess*) = alloc(); + let (squashed_dict_end) = squash_dict{range_check_ptr=range_check_ptr}( + dict_start, dict_end, squashed_dict_start + ); + + // Check the values of the squashed_dict + // should be: {0: (100, 300), 1: (50, 150)} + assert squashed_dict_start[0] = DictAccess(key=0, prev_value=100, new_value=300); + assert squashed_dict_start[1] = DictAccess(key=1, prev_value=50, new_value=150); + return (); +} From 2f7c25595e672aac603ae6591379ba8af76a2022 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Fri, 14 Jun 2024 12:02:44 +0530 Subject: [PATCH 08/10] Use Pop util --- pkg/hintrunner/zero/zerohint_dictionaries.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries.go b/pkg/hintrunner/zero/zerohint_dictionaries.go index ad9a6ef7..313c20f6 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries.go @@ -473,9 +473,11 @@ func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinte if err != nil { return err } - firstKeyValue := keys[len(keys)-1] + firstKeyValue, err := utils.Pop(&keys) + if err != nil { + return err + } firstKeyMv := memory.MemoryValueFromFieldElement(&firstKeyValue) - keys = keys[:len(keys)-1] err = vm.Memory.WriteToAddress(&firstKeyAddr, &firstKeyMv) if err != nil { return err From b7d85626fdc01da93ca772069ff6c9d411961426 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Fri, 14 Jun 2024 12:41:32 +0530 Subject: [PATCH 09/10] Add function docs --- pkg/hintrunner/zero/zerohint_dictionaries.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/hintrunner/zero/zerohint_dictionaries.go b/pkg/hintrunner/zero/zerohint_dictionaries.go index 313c20f6..c8066824 100644 --- a/pkg/hintrunner/zero/zerohint_dictionaries.go +++ b/pkg/hintrunner/zero/zerohint_dictionaries.go @@ -368,6 +368,20 @@ func createDictUpdateHinter(resolver hintReferenceResolver) (hinter.Hinter, erro return newDictUpdateHint(dictPtr, key, newValue, prevValue), nil } +// SquashDict hint as part of the larger dict_squash cairo function does data validation +// and writes to scope a set of variables which indicate the largest used key in the dict, +// a map from key to the list of indices accessing it +// and a descending list of used keys except the largest key. +// It also writes to a cairo variable the largest used key +// and a boolean indicating if any of the keys used were bigger than the range_check +// +// `newSquashDictHint` takes 5 operanders as arguments +// - `dictAccesses` variable will be a pointer to the beginning of an array of DictAccess instances. The format of +// each entry is a triplet (key, prev_value, new_value) +// - `ptrDiff` variable will be the size of the above dictAccesses array +// - `nAccesses` variable will have a value indicating the number of times the dict was accessed +// - `bigKeys` variable will be written a value of 1 if the keys used are bigger than the range_check and 0 otherwise +// - `firstKey` variable will be written the value of the largest used key after the hint is run func newSquashDictHint(dictAccesses, ptrDiff, nAccesses, bigKeys, firstKey hinter.ResOperander) hinter.Hinter { return &GenericZeroHinter{ Name: "SquashDict", From 7a69dfe450901ad5c57310700d1f0bcf59486056 Mon Sep 17 00:00:00 2001 From: Harikrishnan Shaji Date: Mon, 17 Jun 2024 19:00:05 +0530 Subject: [PATCH 10/10] Comment out integration test --- .../cairo_files/squash_dict.cairo | 38 +++++++++++++++++++ .../cairo_files/squash_dict.small.cairo | 33 ---------------- 2 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 integration_tests/cairo_files/squash_dict.cairo delete mode 100644 integration_tests/cairo_files/squash_dict.small.cairo diff --git a/integration_tests/cairo_files/squash_dict.cairo b/integration_tests/cairo_files/squash_dict.cairo new file mode 100644 index 00000000..5b7fa444 --- /dev/null +++ b/integration_tests/cairo_files/squash_dict.cairo @@ -0,0 +1,38 @@ +// %builtins range_check + +// from starkware.cairo.common.squash_dict import squash_dict +// from starkware.cairo.common.alloc import alloc +// from starkware.cairo.common.dict_access import DictAccess + +// func main{range_check_ptr}() -> () { +// alloc_locals; +// let (dict_start: DictAccess*) = alloc(); +// assert dict_start[0] = DictAccess(key=0, prev_value=100, new_value=100); +// assert dict_start[1] = DictAccess(key=1, prev_value=50, new_value=50); +// assert dict_start[2] = DictAccess(key=0, prev_value=100, new_value=200); +// assert dict_start[3] = DictAccess(key=1, prev_value=50, new_value=100); +// assert dict_start[4] = DictAccess(key=0, prev_value=200, new_value=300); +// assert dict_start[5] = DictAccess(key=1, prev_value=100, new_value=150); + +// let dict_end = dict_start + 6 * DictAccess.SIZE; +// // (dict_start, dict_end) now represents the dictionary +// // {0: 100, 1: 50, 0: 200, 1: 100, 0: 300, 1: 150}. + +// // Squash the dictionary from an array of 6 DictAccess structs +// // to an array of 2, with a single DictAccess entry per key. +// let (local squashed_dict_start: DictAccess*) = alloc(); +// let (squashed_dict_end) = squash_dict{range_check_ptr=range_check_ptr}( +// dict_start, dict_end, squashed_dict_start +// ); + +// // Check the values of the squashed_dict +// // should be: {0: (100, 300), 1: (50, 150)} +// assert squashed_dict_start[0] = DictAccess(key=0, prev_value=100, new_value=300); +// assert squashed_dict_start[1] = DictAccess(key=1, prev_value=50, new_value=150); +// return (); +// } + +func main() { + return (); +} + diff --git a/integration_tests/cairo_files/squash_dict.small.cairo b/integration_tests/cairo_files/squash_dict.small.cairo deleted file mode 100644 index 7af92702..00000000 --- a/integration_tests/cairo_files/squash_dict.small.cairo +++ /dev/null @@ -1,33 +0,0 @@ -%builtins range_check - -from starkware.cairo.common.squash_dict import squash_dict -from starkware.cairo.common.alloc import alloc -from starkware.cairo.common.dict_access import DictAccess - -func main{range_check_ptr}() -> () { - alloc_locals; - let (dict_start: DictAccess*) = alloc(); - assert dict_start[0] = DictAccess(key=0, prev_value=100, new_value=100); - assert dict_start[1] = DictAccess(key=1, prev_value=50, new_value=50); - assert dict_start[2] = DictAccess(key=0, prev_value=100, new_value=200); - assert dict_start[3] = DictAccess(key=1, prev_value=50, new_value=100); - assert dict_start[4] = DictAccess(key=0, prev_value=200, new_value=300); - assert dict_start[5] = DictAccess(key=1, prev_value=100, new_value=150); - - let dict_end = dict_start + 6 * DictAccess.SIZE; - // (dict_start, dict_end) now represents the dictionary - // {0: 100, 1: 50, 0: 200, 1: 100, 0: 300, 1: 150}. - - // Squash the dictionary from an array of 6 DictAccess structs - // to an array of 2, with a single DictAccess entry per key. - let (local squashed_dict_start: DictAccess*) = alloc(); - let (squashed_dict_end) = squash_dict{range_check_ptr=range_check_ptr}( - dict_start, dict_end, squashed_dict_start - ); - - // Check the values of the squashed_dict - // should be: {0: (100, 300), 1: (50, 150)} - assert squashed_dict_start[0] = DictAccess(key=0, prev_value=100, new_value=300); - assert squashed_dict_start[1] = DictAccess(key=1, prev_value=50, new_value=150); - return (); -}