# Delving into dynamic-ssz (`dynssz`)
{%preview https://github.com/pk910/dynamic-ssz %}
I found Philip ([pk910](https://github.com/pk910))'s [`dynamic-ssz`](https://github.com/pk910/dynamic-ssz) is in active development recently. This drew my attention as I'm currently working on [SSZ Query Language](https://github.com/eth-protocol-fellows/cohort-six/blob/master/projects/ssz-ql-with-merkle-proofs.md) for any **arbitrary** SSZ object. It's well-documented so that I could learn a lot from this project as well as the rationale for each parts.
## What problem does this project solve?
[`fastssz`](https://github.com/ferranbt/fastssz) is widely used in Go ecosystem regarding SSZ. Prysm also currently uses forked version of it ([OffchainLabs/fastssz](https://github.com/offchainlabs/fastssz/)). `dynssz` leverages on `fastssz` for static encoding/decoding, while also supports dynamic specifications and configurations.
One common usecase is for mainnet/minimal presets. Minimal presets often choose much smaller value for ease of testing. `MAX_COMMITTEES_PER_SLOT` reduces from `64` to `4` when it comes to minimal.
```python
class Attestation(Container):
# [Modified in Electra:EIP7549]
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]
data: AttestationData
signature: BLSSignature
# [New in Electra:EIP7549]
committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT]
```
Using `dynssz`, we can provide different spec values and encode **dynamically** with respect to the context. For marshalling `Attestation`, we **should** know the length of `committee_bits` which is `MAX_COMMITTEES_PER_SLOT`. `dynssz` supports custom Go struct tags like `dynssz-size`, so we can annotate `committee_bits` as `dynssz-size:"MAX_COMMITTEES_PER_SLOT"`.
## Key component: `TypeDescriptor`
```go
// TypeDescriptor represents a cached, optimized descriptor for a type's SSZ encoding/decoding
type TypeDescriptor struct {
Type reflect.Type
Kind reflect.Kind // Go kind of the type
Size uint32 // SSZ size (-1 if dynamic)
Len uint32 // Length of array/slice
Limit uint64 // Limit of array/slice (ssz-max tag)
ContainerDesc *ContainerDescriptor // For structs
ElemDesc *TypeDescriptor // For slices/arrays
HashTreeRootWithMethod *reflect.Method // Cached HashTreeRootWith method for performance
SszType SszType // SSZ type of the type
IsDynamic bool // Whether this type is a dynamic type (or has nested dynamic types)
HasLimit bool // Whether this type has a limit (ssz-max tag)
HasDynamicSize bool // Whether this type uses dynamic spec size value that differs from the default
HasDynamicMax bool // Whether this type uses dynamic spec max value that differs from the default
HasFastSSZMarshaler bool // Whether the type implements fastssz.Marshaler
HasFastSSZHasher bool // Whether the type implements fastssz.HashRoot
HasHashTreeRootWith bool // Whether the type implements HashTreeRootWith
IsPtr bool // Whether this is a pointer type
IsByteArray bool // Whether this is a byte array
IsString bool // Whether this is a string type
}
```
Each SSZ data structure can be represented as one instance of `TypeDescriptor`. It contains all information needed for dealing with SSZ and merklelization.
```go!
// buildTypeDescriptor computes a type descriptor for the given type
func (tc *TypeCache) buildTypeDescriptor(t reflect.Type, sizeHints []SszSizeHint, maxSizeHints []SszMaxSizeHint, typeHints []SszTypeHint) (*TypeDescriptor, error) {
// so on...
}
```
`buildTypeDescriptor` ([Link](https://github.com/pk910/dynamic-ssz/blob/2bc682ea7e2a39f7e0c37d83b8700954c79ba69a/typecache.go#L140)) builds the actual `TypeDescriptor` if the given `reflect.Type` was not "hot" in `TypeCache`.
```go!
if sszType == SszUnspecifiedType {
switch desc.Kind {
// basic types
case reflect.Bool:
sszType = SszBoolType
case reflect.Uint8:
sszType = SszUint8Type
case reflect.Uint16:
sszType = SszUint16Type
case reflect.Uint32:
sszType = SszUint32Type
case reflect.Uint64:
sszType = SszUint64Type
// complex types
case reflect.Struct:
sszType = SszContainerType
case reflect.Array:
sszType = SszVectorType
case reflect.Slice:
if len(sizeHints) > 0 && sizeHints[0].Size > 0 {
sszType = SszVectorType
} else {
sszType = SszListType
}
case reflect.String:
if len(sizeHints) > 0 && sizeHints[0].Size > 0 {
sszType = SszVectorType
} else {
sszType = SszListType
}
desc.IsString = true
// unsupported types
default:
return nil, fmt.Errorf("unsupported type kind: %v", t.Kind())
}
// special case for bitlists
if sszType == SszListType && strings.Contains(t.Name(), "Bitlist") {
sszType = SszBitlistType
}
}
desc.SszType = sszType
```
It first determines `SszType` by the `Kind` of `reflect.Type`. Then, in accordance with `SszType`, it builds the descriptor recursively. For example, `Container` type will be handled like:
```go!
case SszContainerType:
err := tc.buildContainerDescriptor(desc, t)
if err != nil {
return nil, err
}
```
To calculate the size or marshal/unmarshal an object, getting a type descriptor must precede. For example, `SizeSSZ` uses `sourceTypeDesc` and then passes it into `getSszValueSize`, which means it should 1) get from the type cache or 2) build a type descriptor from scratch.
```go
func (d *DynSsz) SizeSSZ(source any) (int, error) {
sourceType := reflect.TypeOf(source)
sourceValue := reflect.ValueOf(source)
sourceTypeDesc, err := d.typeCache.GetTypeDescriptor(sourceType, nil, nil, nil)
if err != nil {
return 0, err
}
size, err := d.getSszValueSize(sourceTypeDesc, sourceValue)
if err != nil {
return 0, err
}
return int(size), nil
}
```
## Some breakthroughs
### `uint256` Support
{%preview https://github.com/pk910/dynamic-ssz/blob/master/docs/api-reference.md#handling-large-integers-uint128uint256 %}
As Go doesn't support `uint128` and `uint256` in its native type system, `dynssz` handles them as a byte array. For example, `uint256` is represented as `[32]uint8 or [4]uint64`.
```go!
// detect some well-known and widely used types
if t.PkgPath() == "github.com/holiman/uint256" && t.Name() == "Int" {
sszType = SszUint256Type
}
```
Also, there's a code for determining the type with its package name, which seems quite hacky but useful.
### Distinguish ambiguous type (e.g., `List` vs. `Bitlist`)
```go!
func (d *DynSsz) getSszTypeTag(field *reflect.StructField) ([]SszTypeHint, error) {
// parse `ssz-type`
sszTypeHints := []SszTypeHint{}
if fieldSszTypeStr, fieldHasSszType := field.Tag.Lookup("ssz-type"); fieldHasSszType {
for _, sszTypeStr := range strings.Split(fieldSszTypeStr, ",") {
sszType := SszTypeHint{}
switch sszTypeStr {
case "?", "auto":
sszType.Type = SszUnspecifiedType
case "custom":
sszType.Type = SszCustomType
case "wrapper", "type-wrapper":
sszType.Type = SszTypeWrapperType
// basic types
case "bool":
sszType.Type = SszBoolType
case "uint8":
sszType.Type = SszUint8Type
case "uint16":
sszType.Type = SszUint16Type
case "uint32":
sszType.Type = SszUint32Type
case "uint64":
sszType.Type = SszUint64Type
case "uint128":
sszType.Type = SszUint128Type
case "uint256":
sszType.Type = SszUint256Type
// complex types
case "container":
sszType.Type = SszContainerType
case "list":
sszType.Type = SszListType
case "vector":
sszType.Type = SszVectorType
case "bitlist":
sszType.Type = SszBitlistType
case "bitvector":
sszType.Type = SszBitvectorType
default:
return nil, fmt.Errorf("invalid ssz-type tag for '%v' field: %v", field.Name, sszTypeStr)
}
sszTypeHints = append(sszTypeHints, sszType)
}
}
return sszTypeHints, nil
}
```
`dynssz` introduces a new Go Struct Tag `ssz-type` for removing ambiguity. In my past experience, it is quite hard, or to be clear, *impossible* to determine between `List` and `Bitlist` from the given struct definition in Prysm. I think this explicit tag can help, while we need to fix the `sszgen` code for supporting it.
## Key takeaways (for SSZ-QL implementation and Prysm context)
- Distinguish between `Uint8 ~ Uint64`, not making it as sole type `UintN`.
- For `uint256`, we don't use `holiman/uint256`. Maybe I need to whitelist field names that use `uint256`...?
- For distinguishing `Bitlist` with `List`, I think I might use the type name like:
```go!
// special case for bitlists
if sszType == SszListType && strings.Contains(t.Name(), "Bitlist") {
sszType = SszBitlistType
}
```