diff --git a/array.go b/array.go index d4e6fca5..62b2aedd 100644 --- a/array.go +++ b/array.go @@ -132,6 +132,7 @@ func (a *ArrayDataSlab) StoredValue(storage SlabStorage) (Value, error) { } var _ ArraySlab = &ArrayDataSlab{} +var _ ContainerStorable = &ArrayDataSlab{} // ArrayMetaDataSlab is internal node, implementing ArraySlab. type ArrayMetaDataSlab struct { @@ -697,14 +698,14 @@ func DecodeInlinedArrayStorable( }, nil } -// encodeAsInlined encodes inlined array data slab. Encoding is +// EncodeAsElement encodes inlined array data slab. Encoding is // version 1 with CBOR tag having tag number CBORTagInlinedArray, // and tag contant as 3-element array: // // +------------------+----------------+----------+ // | extra data index | value ID index | elements | // +------------------+----------------+----------+ -func (a *ArrayDataSlab) encodeAsInlined(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (a *ArrayDataSlab) EncodeAsElement(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { if a.extraData == nil { return NewEncodingError( fmt.Errorf("failed to encode non-root array data slab as inlined")) @@ -754,7 +755,8 @@ func (a *ArrayDataSlab) encodeAsInlined(enc *Encoder, inlinedTypeInfo *inlinedEx // element 2: array elements err = a.encodeElements(enc, inlinedTypeInfo) if err != nil { - return NewEncodingError(err) + // err is already categorized by ArrayDataSlab.encodeElements(). + return err } err = enc.CBOR.Flush() @@ -817,7 +819,7 @@ func (a *ArrayDataSlab) Encode(enc *Encoder) error { return NewEncodingError(err) } - if a.hasPointer() { + if a.HasPointer() { h.setHasPointers() } @@ -885,7 +887,7 @@ func (a *ArrayDataSlab) Encode(enc *Encoder) error { return nil } -func (a *ArrayDataSlab) encodeElements(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (a *ArrayDataSlab) encodeElements(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { // Encode CBOR array size manually for fix-sized encoding enc.Scratch[0] = 0x80 | 25 @@ -906,10 +908,10 @@ func (a *ArrayDataSlab) encodeElements(enc *Encoder, inlinedTypeInfo *inlinedExt // Encode data slab content (array of elements) for _, e := range a.elements { - err = encodeStorableAsElement(enc, e, inlinedTypeInfo) + err = EncodeStorableAsElement(enc, e, inlinedTypeInfo) if err != nil { - // Wrap err as external error (if needed) because err is returned by Storable interface. - return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode array element") + // err is already categorized by encodeStorableAsElement(). + return err } } @@ -1000,7 +1002,7 @@ func (a *ArrayDataSlab) Uninline(storage SlabStorage) error { return nil } -func (a *ArrayDataSlab) hasPointer() bool { +func (a *ArrayDataSlab) HasPointer() bool { for _, e := range a.elements { if hasPointer(e) { return true diff --git a/array_debug.go b/array_debug.go index 18b88556..eb0d2fe7 100644 --- a/array_debug.go +++ b/array_debug.go @@ -345,8 +345,16 @@ func (v *arrayVerifier) verifyDataSlab( } // Verify that only root data slab can be inlined - if level > 0 && dataSlab.Inlined() { - return 0, nil, nil, NewFatalError(fmt.Errorf("non-root slab %s is inlined", id)) + if dataSlab.Inlined() { + if level > 0 { + return 0, nil, nil, NewFatalError(fmt.Errorf("non-root slab %s is inlined", id)) + } + if dataSlab.extraData == nil { + return 0, nil, nil, NewFatalError(fmt.Errorf("inlined slab %s doesn't have extra data", id)) + } + if dataSlab.next != SlabIDUndefined { + return 0, nil, nil, NewFatalError(fmt.Errorf("inlined slab %s has next slab ID", id)) + } } // Verify that aggregated element size + slab prefix is the same as header.size @@ -524,6 +532,11 @@ func VerifyArraySerialization( decodeTypeInfo TypeInfoDecoder, compare StorableComparator, ) error { + // Skip verification of inlined array serialization. + if a.Inlined() { + return nil + } + v := &serializationVerifier{ storage: a.Storage, cborDecMode: cborDecMode, @@ -550,7 +563,7 @@ func (v *serializationVerifier) verifyArraySlab(slab ArraySlab) error { id := slab.SlabID() // Encode slab - data, err := Encode(slab, v.cborEncMode) + data, err := EncodeSlab(slab, v.cborEncMode) if err != nil { // Don't need to wrap error as external error because err is already categorized by Encode(). return err @@ -564,7 +577,7 @@ func (v *serializationVerifier) verifyArraySlab(slab ArraySlab) error { } // Re-encode decoded slab - dataFromDecodedSlab, err := Encode(decodedSlab, v.cborEncMode) + dataFromDecodedSlab, err := EncodeSlab(decodedSlab, v.cborEncMode) if err != nil { // Don't need to wrap error as external error because err is already categorized by Encode(). return err diff --git a/array_test.go b/array_test.go index 588f475e..1a30d718 100644 --- a/array_test.go +++ b/array_test.go @@ -142,6 +142,30 @@ func _testArray( require.Equal(t, 1, len(rootIDs)) require.Equal(t, array.SlabID(), rootIDs[0]) + // Encode all non-nil slab + encodedSlabs := make(map[SlabID][]byte) + for id, slab := range storage.deltas { + if slab != nil { + b, err := EncodeSlab(slab, storage.cborEncMode) + require.NoError(t, err) + encodedSlabs[id] = b + } + } + + // Test decoded array from new storage to force slab decoding + decodedArray, err := NewArrayWithRootID( + newTestPersistentStorageWithBaseStorageAndDeltas(t, storage.baseStorage, encodedSlabs), + array.SlabID()) + require.NoError(t, err) + + // Verify decoded array elements + for i, expected := range expectedValues { + actual, err := decodedArray.Get(uint64(i)) + require.NoError(t, err) + + valueEqual(t, expected, actual) + } + if !hasNestedArrayMapElement { // Need to call Commit before calling storage.Count() for PersistentSlabStorage. err = storage.Commit() diff --git a/cmd/stress/typeinfo.go b/cmd/stress/typeinfo.go index 6a1fafb1..ddeee106 100644 --- a/cmd/stress/typeinfo.go +++ b/cmd/stress/typeinfo.go @@ -30,8 +30,9 @@ const ( maxArrayTypeValue = 10 maxMapTypeValue = 10 - arrayTypeTagNum = 246 - mapTypeTagNum = 245 + arrayTypeTagNum = 246 + mapTypeTagNum = 245 + compositeTypeTagNum = 244 ) type arrayTypeInfo struct { @@ -52,7 +53,7 @@ func (i arrayTypeInfo) IsComposite() bool { return false } -func (i arrayTypeInfo) ID() string { +func (i arrayTypeInfo) Identifier() string { return fmt.Sprintf("array(%d)", i) } @@ -87,7 +88,7 @@ func (i mapTypeInfo) IsComposite() bool { return false } -func (i mapTypeInfo) ID() string { +func (i mapTypeInfo) Identifier() string { return fmt.Sprintf("map(%d)", i) } @@ -104,6 +105,83 @@ func (i mapTypeInfo) Equal(other atree.TypeInfo) bool { return ok && i.value == otherMapTypeInfo.value } +var compositeFieldNames = []string{"a", "b", "c"} + +type compositeTypeInfo struct { + fieldStartIndex int // inclusive start index of fieldNames + fieldEndIndex int // exclusive end index of fieldNames +} + +var _ atree.TypeInfo = mapTypeInfo{} + +// newCompositeTypeInfo creates one of 10 compositeTypeInfo randomly. +// 10 possible composites: +// - ID: composite(0_0), field names: [] +// - ID: composite(0_1), field names: ["a"] +// - ID: composite(0_2), field names: ["a", "b"] +// - ID: composite(0_3), field names: ["a", "b", "c"] +// - ID: composite(1_1), field names: [] +// - ID: composite(1_2), field names: ["b"] +// - ID: composite(1_3), field names: ["b", "c"] +// - ID: composite(2_2), field names: [] +// - ID: composite(2_3), field names: ["c"] +// - ID: composite(3_3), field names: [] +func newCompositeTypeInfo() compositeTypeInfo { + // startIndex is [0, 3] + startIndex := r.Intn(len(compositeFieldNames) + 1) + + // count is [0, 3] + count := r.Intn(len(compositeFieldNames) - startIndex + 1) + + endIndex := startIndex + count + if endIndex > len(compositeFieldNames) { + panic("not reachable") + } + + return compositeTypeInfo{fieldStartIndex: startIndex, fieldEndIndex: endIndex} +} + +func (i compositeTypeInfo) getFieldNames() []string { + return compositeFieldNames[i.fieldStartIndex:i.fieldEndIndex] +} + +func (i compositeTypeInfo) Copy() atree.TypeInfo { + return i +} + +func (i compositeTypeInfo) IsComposite() bool { + return true +} + +func (i compositeTypeInfo) Identifier() string { + return fmt.Sprintf("composite(%d_%d)", i.fieldStartIndex, i.fieldEndIndex) +} + +func (i compositeTypeInfo) Encode(e *cbor.StreamEncoder) error { + err := e.EncodeTagHead(compositeTypeTagNum) + if err != nil { + return err + } + err = e.EncodeArrayHead(2) + if err != nil { + return err + } + err = e.EncodeInt64(int64(i.fieldStartIndex)) + if err != nil { + return err + } + return e.EncodeInt64(int64(i.fieldEndIndex)) +} + +func (i compositeTypeInfo) Equal(other atree.TypeInfo) bool { + otherCompositeTypeInfo, ok := other.(compositeTypeInfo) + if !ok { + return false + } + return i.fieldStartIndex == otherCompositeTypeInfo.fieldStartIndex && + i.fieldEndIndex == otherCompositeTypeInfo.fieldEndIndex +} + func decodeTypeInfo(dec *cbor.StreamDecoder) (atree.TypeInfo, error) { num, err := dec.DecodeTagNumber() if err != nil { @@ -126,6 +204,45 @@ func decodeTypeInfo(dec *cbor.StreamDecoder) (atree.TypeInfo, error) { return mapTypeInfo{value: int(value)}, nil + case compositeTypeTagNum: + count, err := dec.DecodeArrayHead() + if err != nil { + return nil, err + } + if count != 2 { + return nil, fmt.Errorf( + "failed to decode composite type info: expect 2 elemets, got %d elements", + count, + ) + } + + startIndex, err := dec.DecodeInt64() + if err != nil { + return nil, err + } + + endIndex, err := dec.DecodeInt64() + if err != nil { + return nil, err + } + + if endIndex < startIndex { + return nil, fmt.Errorf( + "failed to decode composite type info: endIndex %d < startIndex %d", + endIndex, + startIndex, + ) + } + + if endIndex > int64(len(compositeFieldNames)) { + return nil, fmt.Errorf( + "failed to decode composite type info: endIndex %d > len(compositeFieldNames) %d", + endIndex, + len(compositeFieldNames)) + } + + return compositeTypeInfo{fieldStartIndex: int(startIndex), fieldEndIndex: int(endIndex)}, nil + default: return nil, fmt.Errorf("failed to decode type info with tag number %d", num) } diff --git a/cmd/stress/utils.go b/cmd/stress/utils.go index cd143bd4..e5ffba91 100644 --- a/cmd/stress/utils.go +++ b/cmd/stress/utils.go @@ -47,6 +47,7 @@ const ( const ( arrayType int = iota mapType + compositeType maxContainerValueType ) @@ -123,6 +124,9 @@ func generateContainerValue( length := r.Intn(maxNestedMapSize) return newMap(storage, address, length, nestedLevels) + case compositeType: + return newComposite(storage, address, nestedLevels) + default: return nil, nil, fmt.Errorf("unexpected randome container value type %d", valueType) } @@ -385,6 +389,50 @@ func newMap( return expectedValues, m, nil } +// newComposite creates atree.OrderedMap with elements of random composite type and nested level +func newComposite( + storage atree.SlabStorage, + address atree.Address, + nestedLevel int, +) (mapValue, *atree.OrderedMap, error) { + + compositeType := newCompositeTypeInfo() + + m, err := atree.NewMap(storage, address, atree.NewDefaultDigesterBuilder(), compositeType) + if err != nil { + return nil, nil, fmt.Errorf("failed to create new map: %w", err) + } + + expectedValues := make(mapValue) + + for _, name := range compositeType.getFieldNames() { + + expectedKey, key := NewStringValue(name), NewStringValue(name) + + expectedValue, value, err := randomValue(storage, address, nestedLevel-1) + if err != nil { + return nil, nil, err + } + + expectedValues[expectedKey] = expectedValue + + existingStorable, err := m.Set(compare, hashInputProvider, key, value) + if err != nil { + return nil, nil, err + } + if existingStorable != nil { + return nil, nil, fmt.Errorf("failed to create new map of composite type: found duplicate field name %s", name) + } + } + + err = checkMapDataLoss(expectedValues, m) + if err != nil { + return nil, nil, err + } + + return expectedValues, m, nil +} + type InMemBaseStorage struct { segments map[atree.SlabID][]byte storageIndex map[atree.Address]atree.SlabIndex @@ -492,5 +540,5 @@ func (v mapValue) Storable(atree.SlabStorage, atree.Address, uint64) (atree.Stor } var typeInfoComparator = func(a atree.TypeInfo, b atree.TypeInfo) bool { - return a.ID() == b.ID() + return a.Identifier() == b.Identifier() } diff --git a/encode.go b/encode.go index 5f46505c..7d8eb3c4 100644 --- a/encode.go +++ b/encode.go @@ -42,23 +42,24 @@ func NewEncoder(w io.Writer, encMode cbor.EncMode) *Encoder { } } -// encodeStorableAsElement encodes storable as Array or OrderedMap element. +// EncodeStorableAsElement encodes storable as Array or OrderedMap element. // Storable is encode as an inlined ArrayDataSlab or MapDataSlab if it is ArrayDataSlab or MapDataSlab. -func encodeStorableAsElement(enc *Encoder, storable Storable, inlinedTypeInfo *inlinedExtraData) error { +func EncodeStorableAsElement(enc *Encoder, storable Storable, inlinedTypeInfo *InlinedExtraData) error { switch storable := storable.(type) { - case *ArrayDataSlab: - return storable.encodeAsInlined(enc, inlinedTypeInfo) - - case *MapDataSlab: - return storable.encodeAsInlined(enc, inlinedTypeInfo) + case ContainerStorable: + err := storable.EncodeAsElement(enc, inlinedTypeInfo) + if err != nil { + // Wrap err as external error (if needed) because err is returned by ContainerStorable interface. + return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode container storable as element") + } default: err := storable.Encode(enc) if err != nil { // Wrap err as external error (if needed) because err is returned by Storable interface. - return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode map value") + return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode storable as element") } } diff --git a/map.go b/map.go index 13654279..a4b52e81 100644 --- a/map.go +++ b/map.go @@ -148,7 +148,7 @@ type element interface { key Value, ) (MapKey, MapValue, element, error) - Encode(*Encoder, *inlinedExtraData) error + Encode(*Encoder, *InlinedExtraData) error hasPointer() bool @@ -215,7 +215,7 @@ type elements interface { Element(int) (element, error) - Encode(*Encoder, *inlinedExtraData) error + Encode(*Encoder, *InlinedExtraData) error hasPointer() bool @@ -300,7 +300,7 @@ type MapDataSlab struct { } var _ MapSlab = &MapDataSlab{} -var _ Storable = &MapDataSlab{} +var _ ContainerStorable = &MapDataSlab{} // MapMetaDataSlab is internal node, implementing MapSlab. type MapMetaDataSlab struct { @@ -586,7 +586,7 @@ func newSingleElementFromData(cborDec *cbor.StreamDecoder, decodeStorable Storab // Encode encodes singleElement to the given encoder. // // CBOR encoded array of 2 elements (key, value). -func (e *singleElement) Encode(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (e *singleElement) Encode(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { // Encode CBOR array head for 2 elements err := enc.CBOR.EncodeRawBytes([]byte{0x82}) @@ -595,17 +595,17 @@ func (e *singleElement) Encode(enc *Encoder, inlinedTypeInfo *inlinedExtraData) } // Encode key - err = e.key.Encode(enc) + err = EncodeStorableAsElement(enc, e.key, inlinedTypeInfo) if err != nil { - // Wrap err as external error (if needed) because err is returned by Storable interface. - return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode map key") + // Don't need to wrap error as external error because err is already categorized by encodeStorableAsElement(). + return err } // Encode value - err = encodeStorableAsElement(enc, e.value, inlinedTypeInfo) + err = EncodeStorableAsElement(enc, e.value, inlinedTypeInfo) if err != nil { - // Wrap err as external error (if needed) because err is returned by Storable interface. - return wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode map value") + // Don't need to wrap error as external error because err is already categorized by encodeStorableAsElement(). + return err } err = enc.CBOR.Flush() @@ -763,7 +763,7 @@ func newInlineCollisionGroupFromData(cborDec *cbor.StreamDecoder, decodeStorable // Encode encodes inlineCollisionGroup to the given encoder. // // CBOR tag (number: CBORTagInlineCollisionGroup, content: elements) -func (e *inlineCollisionGroup) Encode(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (e *inlineCollisionGroup) Encode(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { err := enc.CBOR.EncodeRawBytes([]byte{ // tag number CBORTagInlineCollisionGroup @@ -953,7 +953,7 @@ func newExternalCollisionGroupFromData(cborDec *cbor.StreamDecoder, decodeStorab // Encode encodes externalCollisionGroup to the given encoder. // // CBOR tag (number: CBORTagExternalCollisionGroup, content: slab ID) -func (e *externalCollisionGroup) Encode(enc *Encoder, _ *inlinedExtraData) error { +func (e *externalCollisionGroup) Encode(enc *Encoder, _ *InlinedExtraData) error { err := enc.CBOR.EncodeRawBytes([]byte{ // tag number CBORTagExternalCollisionGroup 0xd8, CBORTagExternalCollisionGroup, @@ -1259,7 +1259,7 @@ func newHkeyElementsWithElement(level uint, hkey Digest, elem element) *hkeyElem // 1: hkeys (byte string) // 2: elements (array) // ] -func (e *hkeyElements) Encode(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (e *hkeyElements) Encode(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { if e.level > maxDigestLevel { return NewFatalError(fmt.Errorf("hash level %d exceeds max digest level %d", e.level, maxDigestLevel)) @@ -1921,7 +1921,7 @@ func newSingleElementsWithElement(level uint, elem *singleElement) *singleElemen // 1: hkeys (0 length byte string) // 2: elements (array) // ] -func (e *singleElements) Encode(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (e *singleElements) Encode(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { if e.level > maxDigestLevel { return NewFatalError(fmt.Errorf("digest level %d exceeds max digest level %d", e.level, maxDigestLevel)) @@ -2705,7 +2705,7 @@ func (m *MapDataSlab) Encode(enc *Encoder) error { return NewEncodingError(err) } - if m.hasPointer() { + if m.HasPointer() { h.setHasPointers() } @@ -2777,7 +2777,7 @@ func (m *MapDataSlab) Encode(enc *Encoder) error { return nil } -func (m *MapDataSlab) encodeElements(enc *Encoder, inlinedTypes *inlinedExtraData) error { +func (m *MapDataSlab) encodeElements(enc *Encoder, inlinedTypes *InlinedExtraData) error { err := m.elements.Encode(enc, inlinedTypes) if err != nil { // Don't need to wrap error as external error because err is already categorized by elements.Encode(). @@ -2792,14 +2792,14 @@ func (m *MapDataSlab) encodeElements(enc *Encoder, inlinedTypes *inlinedExtraDat return nil } -// encodeAsInlined encodes inlined map data slab. Encoding is +// EncodeAsElement encodes inlined map data slab. Encoding is // version 1 with CBOR tag having tag number CBORTagInlinedMap, // and tag contant as 3-element array: // // +------------------+----------------+----------+ // | extra data index | value ID index | elements | // +------------------+----------------+----------+ -func (m *MapDataSlab) encodeAsInlined(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (m *MapDataSlab) EncodeAsElement(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { if m.extraData == nil { return NewEncodingError( fmt.Errorf("failed to encode non-root map data slab as inlined")) @@ -2817,7 +2817,7 @@ func (m *MapDataSlab) encodeAsInlined(enc *Encoder, inlinedTypeInfo *inlinedExtr return m.encodeAsInlinedMap(enc, inlinedTypeInfo) } -func (m *MapDataSlab) encodeAsInlinedMap(enc *Encoder, inlinedTypeInfo *inlinedExtraData) error { +func (m *MapDataSlab) encodeAsInlinedMap(enc *Encoder, inlinedTypeInfo *InlinedExtraData) error { extraDataIndex := inlinedTypeInfo.addMapExtraData(m.extraData) @@ -2877,7 +2877,7 @@ func encodeAsInlinedCompactMap( hkeys []Digest, keys []ComparableStorable, values []Storable, - inlinedTypeInfo *inlinedExtraData, + inlinedTypeInfo *InlinedExtraData, ) error { extraDataIndex, cachedKeys := inlinedTypeInfo.addCompactMapExtraData(extraData, hkeys, keys) @@ -2923,7 +2923,8 @@ func encodeAsInlinedCompactMap( // element 2: compact map values in the order of cachedKeys err = encodeCompactMapValues(enc, cachedKeys, keys, values, inlinedTypeInfo) if err != nil { - return NewEncodingError(err) + // err is already categorized by encodeCompactMapValues(). + return err } err = enc.CBOR.Flush() @@ -2940,7 +2941,7 @@ func encodeCompactMapValues( cachedKeys []ComparableStorable, keys []ComparableStorable, values []Storable, - inlinedTypeInfo *inlinedExtraData, + inlinedTypeInfo *InlinedExtraData, ) error { var err error @@ -2966,9 +2967,9 @@ func encodeCompactMapValues( found = true keyIndexes[i], keyIndexes[j] = keyIndexes[j], keyIndexes[i] - err = encodeStorableAsElement(enc, values[index], inlinedTypeInfo) + err = EncodeStorableAsElement(enc, values[index], inlinedTypeInfo) if err != nil { - // Don't need to wrap error as external error because err is already categorized by encodeStorable(). + // Don't need to wrap error as external error because err is already categorized by encodeStorableAsElement(). return err } @@ -3030,7 +3031,7 @@ func (m *MapDataSlab) canBeEncodedAsCompactMap() ([]Digest, []ComparableStorable return elements.hkeys, keys, values, true } -func (m *MapDataSlab) hasPointer() bool { +func (m *MapDataSlab) HasPointer() bool { return m.elements.hasPointer() } diff --git a/map_debug.go b/map_debug.go index 3b444350..dbfd6834 100644 --- a/map_debug.go +++ b/map_debug.go @@ -470,8 +470,16 @@ func (v *mapVerifier) verifyDataSlab( } // Verify that only root slab can be inlined - if level > 0 && dataSlab.Inlined() { - return 0, nil, nil, nil, NewFatalError(fmt.Errorf("non-root slab %s is inlined", id)) + if dataSlab.Inlined() { + if level > 0 { + return 0, nil, nil, nil, NewFatalError(fmt.Errorf("non-root slab %s is inlined", id)) + } + if dataSlab.extraData == nil { + return 0, nil, nil, nil, NewFatalError(fmt.Errorf("inlined slab %s doesn't have extra data", id)) + } + if dataSlab.next != SlabIDUndefined { + return 0, nil, nil, nil, NewFatalError(fmt.Errorf("inlined slab %s has next slab ID", id)) + } } // Verify that aggregated element size + slab prefix is the same as header.size @@ -846,12 +854,6 @@ func (v *mapVerifier) verifySingleElement( return 0, 0, wrapErrorfAsExternalErrorIfNeeded(err, fmt.Sprintf("element %s key can't be converted to value", e)) } - switch e.key.(type) { - case *ArrayDataSlab, *MapDataSlab: - // Verify key can't be inlined array or map - return 0, 0, NewFatalError(fmt.Errorf("element %s key shouldn't be inlined array or map", e)) - } - err = verifyValue(kv, v.address, nil, v.tic, v.hip, v.inlineEnabled, slabIDs) if err != nil { // Don't need to wrap error as external error because err is already categorized by verifyValue(). @@ -942,6 +944,11 @@ func VerifyMapSerialization( decodeTypeInfo TypeInfoDecoder, compare StorableComparator, ) error { + // Skip verification of inlined map serialization. + if m.Inlined() { + return nil + } + v := &serializationVerifier{ storage: m.Storage, cborDecMode: cborDecMode, @@ -958,7 +965,7 @@ func (v *serializationVerifier) verifyMapSlab(slab MapSlab) error { id := slab.SlabID() // Encode slab - data, err := Encode(slab, v.cborEncMode) + data, err := EncodeSlab(slab, v.cborEncMode) if err != nil { // Don't need to wrap error as external error because err is already categorized by Encode(). return err @@ -972,7 +979,7 @@ func (v *serializationVerifier) verifyMapSlab(slab MapSlab) error { } // Re-encode decoded slab - dataFromDecodedSlab, err := Encode(decodedSlab, v.cborEncMode) + dataFromDecodedSlab, err := EncodeSlab(decodedSlab, v.cborEncMode) if err != nil { // Don't need to wrap error as external error because err is already categorized by Encode(). return err @@ -1065,8 +1072,10 @@ func (v *serializationVerifier) verifyMapSlab(slab MapSlab) error { func (v *serializationVerifier) mapDataSlabEqual(expected, actual *MapDataSlab) error { + _, _, _, actualDecodedFromCompactMap := expected.canBeEncodedAsCompactMap() + // Compare extra data - err := mapExtraDataEqual(expected.extraData, actual.extraData) + err := mapExtraDataEqual(expected.extraData, actual.extraData, actualDecodedFromCompactMap) if err != nil { // Don't need to wrap error as external error because err is already categorized by mapExtraDataEqual(). return err @@ -1093,12 +1102,19 @@ func (v *serializationVerifier) mapDataSlabEqual(expected, actual *MapDataSlab) } // Compare header - if !reflect.DeepEqual(expected.header, actual.header) { + if actualDecodedFromCompactMap { + if expected.header.slabID != actual.header.slabID { + return NewFatalError(fmt.Errorf("header.slabID %s is wrong, want %s", actual.header.slabID, expected.header.slabID)) + } + if expected.header.size != actual.header.size { + return NewFatalError(fmt.Errorf("header.size %d is wrong, want %d", actual.header.size, expected.header.size)) + } + } else if !reflect.DeepEqual(expected.header, actual.header) { return NewFatalError(fmt.Errorf("header %+v is wrong, want %+v", actual.header, expected.header)) } // Compare elements - err = v.mapElementsEqual(expected.elements, actual.elements) + err = v.mapElementsEqual(expected.elements, actual.elements, actualDecodedFromCompactMap) if err != nil { // Don't need to wrap error as external error because err is already categorized by mapElementsEqual(). return err @@ -1107,7 +1123,7 @@ func (v *serializationVerifier) mapDataSlabEqual(expected, actual *MapDataSlab) return nil } -func (v *serializationVerifier) mapElementsEqual(expected, actual elements) error { +func (v *serializationVerifier) mapElementsEqual(expected, actual elements, actualDecodedFromCompactMap bool) error { switch expectedElems := expected.(type) { case *hkeyElements: @@ -1115,7 +1131,7 @@ func (v *serializationVerifier) mapElementsEqual(expected, actual elements) erro if !ok { return NewFatalError(fmt.Errorf("elements type %T is wrong, want %T", actual, expected)) } - return v.mapHkeyElementsEqual(expectedElems, actualElems) + return v.mapHkeyElementsEqual(expectedElems, actualElems, actualDecodedFromCompactMap) case *singleElements: actualElems, ok := actual.(*singleElements) @@ -1129,7 +1145,7 @@ func (v *serializationVerifier) mapElementsEqual(expected, actual elements) erro return nil } -func (v *serializationVerifier) mapHkeyElementsEqual(expected, actual *hkeyElements) error { +func (v *serializationVerifier) mapHkeyElementsEqual(expected, actual *hkeyElements, actualDecodedFromCompactMap bool) error { if expected.level != actual.level { return NewFatalError(fmt.Errorf("hkeyElements level %d is wrong, want %d", actual.level, expected.level)) @@ -1139,12 +1155,12 @@ func (v *serializationVerifier) mapHkeyElementsEqual(expected, actual *hkeyEleme return NewFatalError(fmt.Errorf("hkeyElements size %d is wrong, want %d", actual.size, expected.size)) } - if len(expected.hkeys) == 0 { - if len(actual.hkeys) != 0 { - return NewFatalError(fmt.Errorf("hkeyElements hkeys %v is wrong, want %v", actual.hkeys, expected.hkeys)) - } - } else { - if !reflect.DeepEqual(expected.hkeys, actual.hkeys) { + if len(expected.hkeys) != len(actual.hkeys) { + return NewFatalError(fmt.Errorf("hkeyElements hkeys len %d is wrong, want %d", len(actual.hkeys), len(expected.hkeys))) + } + + if !actualDecodedFromCompactMap { + if len(expected.hkeys) > 0 && !reflect.DeepEqual(expected.hkeys, actual.hkeys) { return NewFatalError(fmt.Errorf("hkeyElements hkeys %v is wrong, want %v", actual.hkeys, expected.hkeys)) } } @@ -1153,14 +1169,30 @@ func (v *serializationVerifier) mapHkeyElementsEqual(expected, actual *hkeyEleme return NewFatalError(fmt.Errorf("hkeyElements elems len %d is wrong, want %d", len(actual.elems), len(expected.elems))) } - for i := 0; i < len(expected.elems); i++ { - expectedEle := expected.elems[i] - actualEle := actual.elems[i] + if actualDecodedFromCompactMap { + for _, expectedEle := range expected.elems { + found := false + for _, actualEle := range actual.elems { + err := v.mapElementEqual(expectedEle, actualEle, actualDecodedFromCompactMap) + if err == nil { + found = true + break + } + } + if !found { + return NewFatalError(fmt.Errorf("hkeyElements elem %v is not found", expectedEle)) + } + } + } else { + for i := 0; i < len(expected.elems); i++ { + expectedEle := expected.elems[i] + actualEle := actual.elems[i] - err := v.mapElementEqual(expectedEle, actualEle) - if err != nil { - // Don't need to wrap error as external error because err is already categorized by mapElementEqual(). - return err + err := v.mapElementEqual(expectedEle, actualEle, actualDecodedFromCompactMap) + if err != nil { + // Don't need to wrap error as external error because err is already categorized by mapElementEqual(). + return err + } } } @@ -1195,7 +1227,7 @@ func (v *serializationVerifier) mapSingleElementsEqual(expected, actual *singleE return nil } -func (v *serializationVerifier) mapElementEqual(expected, actual element) error { +func (v *serializationVerifier) mapElementEqual(expected, actual element, actualDecodedFromCompactMap bool) error { switch expectedElem := expected.(type) { case *singleElement: @@ -1210,7 +1242,7 @@ func (v *serializationVerifier) mapElementEqual(expected, actual element) error if !ok { return NewFatalError(fmt.Errorf("elements type %T is wrong, want %T", actual, expected)) } - return v.mapElementsEqual(expectedElem.elements, actualElem.elements) + return v.mapElementsEqual(expectedElem.elements, actualElem.elements, actualDecodedFromCompactMap) case *externalCollisionGroup: actualElem, ok := actual.(*externalCollisionGroup) @@ -1322,7 +1354,7 @@ func (v *serializationVerifier) mapSingleElementEqual(expected, actual *singleEl func (v *serializationVerifier) mapMetaDataSlabEqual(expected, actual *MapMetaDataSlab) error { // Compare extra data - err := mapExtraDataEqual(expected.extraData, actual.extraData) + err := mapExtraDataEqual(expected.extraData, actual.extraData, false) if err != nil { // Don't need to wrap error as external error because err is already categorized by mapExtraDataEqual(). return err @@ -1341,7 +1373,7 @@ func (v *serializationVerifier) mapMetaDataSlabEqual(expected, actual *MapMetaDa return nil } -func mapExtraDataEqual(expected, actual *MapExtraData) error { +func mapExtraDataEqual(expected, actual *MapExtraData, actualDecodedFromCompactMap bool) error { if (expected == nil) && (actual == nil) { return nil @@ -1351,8 +1383,18 @@ func mapExtraDataEqual(expected, actual *MapExtraData) error { return NewFatalError(fmt.Errorf("has extra data is %t, want %t", actual == nil, expected == nil)) } - if !reflect.DeepEqual(*expected, *actual) { - return NewFatalError(fmt.Errorf("extra data %+v is wrong, want %+v", *actual, *expected)) + if !reflect.DeepEqual(expected.TypeInfo, actual.TypeInfo) { + return NewFatalError(fmt.Errorf("map extra data type %+v is wrong, want %+v", actual.TypeInfo, expected.TypeInfo)) + } + + if expected.Count != actual.Count { + return NewFatalError(fmt.Errorf("map extra data count %d is wrong, want %d", actual.Count, expected.Count)) + } + + if !actualDecodedFromCompactMap { + if expected.Seed != actual.Seed { + return NewFatalError(fmt.Errorf("map extra data seed %d is wrong, want %d", actual.Seed, expected.Seed)) + } } return nil diff --git a/map_test.go b/map_test.go index 30d25ccb..dccfd4bc 100644 --- a/map_test.go +++ b/map_test.go @@ -215,6 +215,31 @@ func _testMap( require.Equal(t, 1, len(rootIDs)) require.Equal(t, m.SlabID(), rootIDs[0]) + // Encode all non-nil slab + encodedSlabs := make(map[SlabID][]byte) + for id, slab := range storage.deltas { + if slab != nil { + b, err := EncodeSlab(slab, storage.cborEncMode) + require.NoError(t, err) + encodedSlabs[id] = b + } + } + + // Test decoded map from new storage to force slab decoding + decodedMap, err := NewMapWithRootID( + newTestPersistentStorageWithBaseStorageAndDeltas(t, storage.baseStorage, encodedSlabs), + m.SlabID(), + m.digesterBuilder) + require.NoError(t, err) + + // Verify decoded map elements + for k, expected := range expectedKeyValues { + actual, err := decodedMap.Get(compare, hashInputProvider, k) + require.NoError(t, err) + + valueEqual(t, expected, actual) + } + if !hasNestedArrayMapElement { // Need to call Commit before calling storage.Count() for PersistentSlabStorage. err = storage.Commit() diff --git a/storable.go b/storable.go index 02888130..34f83648 100644 --- a/storable.go +++ b/storable.go @@ -54,14 +54,23 @@ type ComparableStorable interface { Copy() Storable } -type containerStorable interface { +// ContainerStorable is an interface that supports Storable containing other storables. +type ContainerStorable interface { Storable - hasPointer() bool + + // EncodeAsElement encodes ContainerStorable and its child storables as an element + // of parent array/map. Since child storable can be inlined array or map, + // encoding inlined array or map requires extra parameter InlinedExtraData. + EncodeAsElement(*Encoder, *InlinedExtraData) error + + // HasPointer returns true if any of its child storables is SlabIDStorable + // (references to another slab). This function is used during encoding. + HasPointer() bool } func hasPointer(storable Storable) bool { - if cs, ok := storable.(containerStorable); ok { - return cs.hasPointer() + if cs, ok := storable.(ContainerStorable); ok { + return cs.HasPointer() } return false } @@ -88,10 +97,9 @@ const ( type SlabIDStorable SlabID -var _ Storable = SlabIDStorable{} -var _ containerStorable = SlabIDStorable{} +var _ ContainerStorable = SlabIDStorable{} -func (v SlabIDStorable) hasPointer() bool { +func (v SlabIDStorable) HasPointer() bool { return true } @@ -148,6 +156,10 @@ func (v SlabIDStorable) Encode(enc *Encoder) error { return nil } +func (v SlabIDStorable) EncodeAsElement(enc *Encoder, _ *InlinedExtraData) error { + return v.Encode(enc) +} + func (v SlabIDStorable) ByteSize() uint32 { // tag number (2 bytes) + byte string header (1 byte) + slab id (16 bytes) return 2 + 1 + slabIDSize @@ -157,12 +169,11 @@ func (v SlabIDStorable) String() string { return fmt.Sprintf("SlabIDStorable(%d)", v) } -// Encode is a wrapper for Storable.Encode() -func Encode(storable Storable, encMode cbor.EncMode) ([]byte, error) { +func EncodeSlab(slab Slab, encMode cbor.EncMode) ([]byte, error) { var buf bytes.Buffer enc := NewEncoder(&buf, encMode) - err := storable.Encode(enc) + err := slab.Encode(enc) if err != nil { // Wrap err as external error (if needed) because err is returned by Storable interface. return nil, wrapErrorfAsExternalErrorIfNeeded(err, "failed to encode storable") diff --git a/storable_test.go b/storable_test.go index 12b732f2..1e747120 100644 --- a/storable_test.go +++ b/storable_test.go @@ -698,11 +698,11 @@ type SomeStorable struct { Storable Storable } -var _ Storable = SomeStorable{} +var _ ContainerStorable = SomeStorable{} -func (v SomeStorable) hasPointer() bool { - if ms, ok := v.Storable.(containerStorable); ok { - return ms.hasPointer() +func (v SomeStorable) HasPointer() bool { + if ms, ok := v.Storable.(ContainerStorable); ok { + return ms.HasPointer() } return false } @@ -723,6 +723,17 @@ func (v SomeStorable) Encode(enc *Encoder) error { return v.Storable.Encode(enc) } +func (v SomeStorable) EncodeAsElement(enc *Encoder, inlinedExtraData *InlinedExtraData) error { + err := enc.CBOR.EncodeRawBytes([]byte{ + // tag number + 0xd8, cborTagSomeValue, + }) + if err != nil { + return err + } + return EncodeStorableAsElement(enc, v.Storable, inlinedExtraData) +} + func (v SomeStorable) ChildStorables() []Storable { return []Storable{v.Storable} } diff --git a/storage.go b/storage.go index 42ca7490..0b324381 100644 --- a/storage.go +++ b/storage.go @@ -390,7 +390,7 @@ func (s *BasicSlabStorage) SlabIDs() []SlabID { func (s *BasicSlabStorage) Encode() (map[SlabID][]byte, error) { m := make(map[SlabID][]byte) for id, slab := range s.Slabs { - b, err := Encode(slab, s.cborEncMode) + b, err := EncodeSlab(slab, s.cborEncMode) if err != nil { // err is already categorized by Encode(). return nil, err @@ -800,7 +800,7 @@ func (s *PersistentSlabStorage) Commit() error { } // serialize - data, err := Encode(slab, s.cborEncMode) + data, err := EncodeSlab(slab, s.cborEncMode) if err != nil { // err is categorized already by Encode() return err @@ -879,7 +879,7 @@ func (s *PersistentSlabStorage) FastCommit(numWorkers int) error { continue } // serialize - data, err := Encode(slab, s.cborEncMode) + data, err := EncodeSlab(slab, s.cborEncMode) results <- &encodedSlabs{ slabID: id, data: data, diff --git a/storage_test.go b/storage_test.go index 2cd2a929..2b90bb11 100644 --- a/storage_test.go +++ b/storage_test.go @@ -749,7 +749,7 @@ func TestPersistentStorage(t *testing.T) { require.NoError(t, err) // capture data for accuracy testing - simpleMap[slabID], err = Encode(slab, encMode) + simpleMap[slabID], err = EncodeSlab(slab, encMode) require.NoError(t, err) } } @@ -1000,7 +1000,7 @@ func TestPersistentStorageSlabIterator(t *testing.T) { break } - encodedSlab, err := Encode(slab, storage.cborEncMode) + encodedSlab, err := EncodeSlab(slab, storage.cborEncMode) require.NoError(t, err) require.Equal(t, encodedSlab, data[id]) diff --git a/typeinfo.go b/typeinfo.go index cabb1469..86c9fe67 100644 --- a/typeinfo.go +++ b/typeinfo.go @@ -22,6 +22,7 @@ import ( "encoding/binary" "fmt" "sort" + "strings" "github.com/fxamacker/cbor/v2" ) @@ -29,7 +30,7 @@ import ( type TypeInfo interface { Encode(*cbor.StreamEncoder) error IsComposite() bool - ID() string + Identifier() string Copy() TypeInfo } @@ -199,18 +200,18 @@ type compactMapTypeInfo struct { keys []ComparableStorable } -type inlinedExtraData struct { +type InlinedExtraData struct { extraData []ExtraData compactMapTypes map[string]compactMapTypeInfo arrayTypes map[string]int } -func newInlinedExtraData() *inlinedExtraData { - return &inlinedExtraData{} +func newInlinedExtraData() *InlinedExtraData { + return &InlinedExtraData{} } // Encode encodes inlined extra data as CBOR array. -func (ied *inlinedExtraData) Encode(enc *Encoder) error { +func (ied *InlinedExtraData) Encode(enc *Encoder) error { err := enc.CBOR.EncodeArrayHead(uint64(len(ied.extraData))) if err != nil { return NewEncodingError(err) @@ -307,12 +308,12 @@ func newInlinedExtraDataFromData( // addArrayExtraData returns index of deduplicated array extra data. // Array extra data is deduplicated by array type info ID because array // extra data only contains type info. -func (ied *inlinedExtraData) addArrayExtraData(data *ArrayExtraData) int { +func (ied *InlinedExtraData) addArrayExtraData(data *ArrayExtraData) int { if ied.arrayTypes == nil { ied.arrayTypes = make(map[string]int) } - id := data.TypeInfo.ID() + id := data.TypeInfo.Identifier() index, exist := ied.arrayTypes[id] if exist { return index @@ -326,7 +327,7 @@ func (ied *inlinedExtraData) addArrayExtraData(data *ArrayExtraData) int { // addMapExtraData returns index of map extra data. // Map extra data is not deduplicated because it also contains count and seed. -func (ied *inlinedExtraData) addMapExtraData(data *MapExtraData) int { +func (ied *InlinedExtraData) addMapExtraData(data *MapExtraData) int { index := len(ied.extraData) ied.extraData = append(ied.extraData, data) return index @@ -334,7 +335,7 @@ func (ied *inlinedExtraData) addMapExtraData(data *MapExtraData) int { // addCompactMapExtraData returns index of deduplicated compact map extra data. // Compact map extra data is deduplicated by TypeInfo.ID() with sorted field names. -func (ied *inlinedExtraData) addCompactMapExtraData( +func (ied *InlinedExtraData) addCompactMapExtraData( data *MapExtraData, digests []Digest, keys []ComparableStorable, @@ -367,7 +368,7 @@ func (ied *inlinedExtraData) addCompactMapExtraData( return index, keys } -func (ied *inlinedExtraData) empty() bool { +func (ied *InlinedExtraData) empty() bool { return len(ied.extraData) == 0 } @@ -376,14 +377,14 @@ func makeCompactMapTypeID(t TypeInfo, names []ComparableStorable) string { const separator = "," if len(names) == 1 { - return t.ID() + separator + names[0].ID() + return t.Identifier() + separator + names[0].ID() } sorter := newFieldNameSorter(names) sort.Sort(sorter) - return t.ID() + separator + sorter.join(separator) + return t.Identifier() + separator + sorter.join(separator) } // fieldNameSorter sorts names by index (not in place sort). @@ -418,9 +419,12 @@ func (fn *fieldNameSorter) Swap(i, j int) { } func (fn *fieldNameSorter) join(sep string) string { - var s string - for _, i := range fn.index { - s += sep + fn.names[i].ID() + var sb strings.Builder + for i, index := range fn.index { + if i > 0 { + sb.WriteString(sep) + } + sb.WriteString(fn.names[index].ID()) } - return s + return sb.String() } diff --git a/utils_test.go b/utils_test.go index 5762b3d3..84aba2c6 100644 --- a/utils_test.go +++ b/utils_test.go @@ -100,7 +100,7 @@ func (i testTypeInfo) IsComposite() bool { return false } -func (i testTypeInfo) ID() string { +func (i testTypeInfo) Identifier() string { return fmt.Sprintf("uint64(%d)", i) } @@ -129,7 +129,7 @@ func (i testCompositeTypeInfo) IsComposite() bool { return true } -func (i testCompositeTypeInfo) ID() string { +func (i testCompositeTypeInfo) Identifier() string { return fmt.Sprintf("composite(%d)", i) } @@ -200,6 +200,15 @@ func newTestPersistentStorageWithBaseStorage(t testing.TB, baseStorage BaseStora ) } +func newTestPersistentStorageWithBaseStorageAndDeltas(t testing.TB, baseStorage BaseStorage, data map[SlabID][]byte) *PersistentSlabStorage { + storage := newTestPersistentStorageWithBaseStorage(t, baseStorage) + for id, b := range data { + err := storage.baseStorage.Store(id, b) + require.NoError(t, err) + } + return storage +} + func newTestBasicStorage(t testing.TB) *BasicSlabStorage { encMode, err := cbor.EncOptions{}.EncMode() require.NoError(t, err)