github.com/protolambda/zssz@v0.1.5/README.md (about) 1 # ZSSZ 2 3 Highly optimized SSZ encoder. Uses unsafe Go, safely. 4 5 "ZSSZ", a.k.a. ZRNT-SSZ, is the SSZ version used and maintained for [ZRNT](https://github.com/protolambda/zrnt), 6 the ETH 2.0 Go executable spec. 7 8 9 Features: 10 - Zero-allocations where possible 11 - offset checking allocates a small array of `uint32`s 12 - dynamic size uses buffer pooling to encode dynamic data *in order* 13 - small value passing is preferred over passing slices, avoid memory on the heap. 14 - Construct all encoding/decoding/hashing logic for a type, then run it 10000 times the efficient way 15 - No reflection during encoding/decoding/hashing execution of the constructed SSZ-type 16 - Exception: slice allocation uses `reflect.MakeSlice`, and pointer-value allocation uses `reflect.New`, 17 but the type is already readily available. 18 This is to avoid the GC collecting allocated space within a slice, and be safe on memory assumptions. 19 - Bitfields/Bitlists are compatible with byte arrays, and not unpacked into boolean arrays. 20 - Stable merkleization, using the v0.8 limit-based merkleization depth for lists. 21 - Construction of SSZ types can also be used to support encoding of dynamic types 22 - Support for struct embedding & squashing. 23 - No dependencies other than the standard Go libs. 24 - Zero-hashes are pre-computed with the `sha256` package, 25 but you can supply a more efficient version to run hash-tree-root with. 26 (E.g. reduce allocations by re-using a single state) 27 - Hardened: A work in progress now, but all SSZ rules are strictly yet efficiently enforced. 28 - Fuzzmode-decoding: decode arbitrary data into a struct. 29 The length of the input + contents determine the length of dynamic parts. 30 - Replaceable hash-function. Initialize the pre-computed zero-hashes with `InitZeroHashes(yourHashFn)` 31 and then call `HashTreeRoot(yourHashFn, val, sszType)`. Zero-hashes default to SHA-256. 32 - Passes the Eth 2.0 Static-SSZ tests, in the [ZRNT](https://github.com/protolambda/zrnt) test suite. 33 - Ongoing real-world benchmarking effort for use in ZRNT. 34 - No caching implemented, *yet*. Work in progress, in open collaboration with ETH 2.0 client implementers. 35 36 Supported types 37 - small basic-types (`bool`, `uint8`, `uint16`, `uint32`, `uint64`) 38 - containers 39 - squash non-pointer struct-fields with a tag `ssz:"squash"`, or embed the struct. 40 Note: just like field names must be public, embedded structs must be a public type. 41 - vector 42 - `Vector`, optimized for fixed and variable size elements 43 - `BasicVector`, optimized for basic element types 44 - `BytesN`, optimized for bytes vector (fixed length) 45 - `Bitvector`, bits packed in a byte array. 46 - list 47 - `List` optimized for fixed and variable size elements 48 - `BasicList`, optimized for basic element types 49 - `Bytes`, optimized for bytes list (dynamic length) 50 - `Bitlist`, bits packed in a byte slice, with bit delimiter to determine length. 51 52 Possibly supported in future: 53 - union/null 54 - embedding of pointer structs (suggestions for nil-semantics welcome) 55 - uint128/uint256 56 - strings 57 - partials 58 59 60 ## Usage 61 62 Here is an example that illustrates the full usage: 63 ```go 64 package main 65 66 import ( 67 . "github.com/protolambda/zssz/types" 68 . "github.com/protolambda/zssz/htr" 69 . "github.com/protolambda/zssz" 70 "reflect" 71 "bytes" 72 "bufio" 73 "fmt" 74 "crypto/sha256" 75 ) 76 77 type Bar struct { 78 DogeImg []byte 79 Timestamp uint64 80 ID [32]byte 81 } 82 83 type MyThing struct { 84 Foo uint32 85 Bars [2]Bar 86 SomeSignature [96]byte 87 } 88 89 func main() { 90 myThingSSZ := GetSSZ((*MyThing)(nil)) 91 92 // example instance filled with dummy data 93 obj := MyThing{ 94 Foo: 123, 95 Bars: [2]Bar{ 96 {DogeImg: []byte{1, 2, 3}, Timestamp: 0x112233, ID: [32]byte{1}}, 97 {DogeImg: []byte{7, 8, 9}, Timestamp: 12345678, ID: [32]byte{2}}, 98 }, 99 SomeSignature: [96]byte{0,1,2,3,4,5,6,7,8,9}, 100 } 101 102 // encoding 103 // ----------------------- 104 var buf bytes.Buffer 105 bufWriter := bufio.NewWriter(&buf) 106 // Note that Encode takes any io.Writer 107 if err := Encode(bufWriter, &obj, myThingSSZ); err != nil { 108 panic(err) 109 } 110 if err := bufWriter.Flush(); err != nil { 111 panic(err) 112 } 113 data := buf.Bytes() 114 fmt.Printf("encoded myThing: %x\n", data) 115 116 // decoding 117 // ----------------------- 118 dst := MyThing{} 119 bytesLen := uint32(len(data)) 120 r := bytes.NewReader(data) 121 // note that Decode takes any io.Reader 122 if err := Decode(r, bytesLen, &dst, myThingSSZ); err != nil { 123 panic(err) 124 } 125 fmt.Printf("decoded myThing: some data from it: %v\n", dst.Bars[1].DogeImg[:]) 126 127 // hash-tree-root 128 // ----------------------- 129 // get the root 130 root := HashTreeRoot(sha256.Sum256, &obj, myThingSSZ) 131 fmt.Printf("root of my thing: %x\n", root) 132 133 signingRoot := SigningRoot(sha256.Sum256, &obj, myThingSSZ.(SignedSSZ)) 134 fmt.Printf("signing-root of my thing: %x\n", signingRoot) 135 } 136 ``` 137 138 ## Format 139 140 ### Basic types 141 142 SSZ basic types match the Go types, with the exception of `uint128` and `uint256`, which are not supported. 143 As a replacement, these can be declared as `[16]byte` and `[32]byte`, and will encode/decode/hash the same. 144 145 ### Lists 146 147 Lists types are required to define a limit. 148 For sanity in an adversarial environment, and stable merkleization. 149 150 Since Go does not support parametrizing types, meta-classes, or any static class functionality, 151 this is slightly non-standard: by defining a pointer-receiver method on the type, 152 we can return the (static) limit information, even if the pointer itself is nil. 153 This enables ZSSZ to derive the data from a typed nil-pointer. 154 155 This pattern also enables to deal with the list like any other slice, 156 and not create custom constructor/parsers/serializers for it elsewhere. 157 158 Example: 159 ```go 160 type MyList []Something 161 162 func (_ *MyList) Limit() uint32 { 163 return 256 164 } 165 166 type SomeBytes []byte 167 168 func (_ *SomeBytes) Limit() uint32 { 169 return 1024 170 } 171 172 type RootList [][32]byte 173 174 func (_ *RootList) Limit() uint32 { 175 return 1 << 13 176 } 177 ``` 178 179 ### Containers 180 181 Containers are just structs. Fields can be omitted by adding `ssz:"omit"` as struct-field tag. 182 183 ```go 184 type MyContainer struct { 185 Foo uint64 186 Cache NonSSZThing `ssz:"omit"` 187 Bar OtherContainer 188 Abc MyList 189 } 190 ``` 191 192 ### Bitfields 193 194 Series of bools are too inefficient, hence SSZ defines a way to pack bools in a bitfield. 195 This memory structure is simple and easily implemented in Go, hence the choice to not unpack into a bool array. 196 This preserves all compatibility with byte arrays, which is nice for debugging, 197 and using other serialization formats without any special encoders or wrappers. 198 199 The `bitfiels` package contains interfaces and helper methods to implement the essential 200 bitfield functionality in your own bitvector/bitlist types. 201 202 #### Bitvectors 203 204 Bitvectors are defined as byte vectors, tagged with some extra information (a `BitLen() uint32` function). 205 206 Example: 207 208 ```go 209 type Bitvec4 [1]byte 210 211 func (_ *Bitvec4) BitLen() uint32 { return 4 } 212 213 const MY_LARGE_BITVECTOR_SIZE = 10000 214 215 type LargeBitfield [(MY_LARGE_BITVECTOR_SIZE + 7) / 8]byte 216 217 func (_ *LargeBitfield) BitLen() uint32 { return MY_LARGE_BITVECTOR_SIZE } 218 ``` 219 220 #### Bitlists 221 222 Bitlists are like bitvectors, but based off of byte slices instead. 223 Bitlists are dynamic in size, and hence need a limit, like any other list type. 224 To determine the length in bits, the last byte of a bitlist has a trailing (high end of byte) `1` bit 225 that functions as delimiter. There is an utility function to read it. 226 227 Example: 228 229 ```go 230 type bitlist16 []byte 231 232 func (_ *bitlist16) Limit() uint32 { return 16 } 233 func (b bitlist16) BitLen() uint32 { return bitfields.BitlistLen(b) } 234 ``` 235 236 ### Union 237 238 Not yet supported. Suggestions for Go-style are welcome. 239 240 241 ## Extending 242 243 ZRNT-SSZ does not check for interfaces on types to do custom SSZ encoding etc., as this would be to slow and inflexible. 244 245 Instead, it gives you the freedom to compose your own custom SSZ type-factory, 246 the function that is used to compose a `SSZ` structure. 247 248 The default factory function is building the structure when calling `GetSSZ((*SomeType)(nil))` 249 250 With this, you can remove/change existing functionality, and add your own. 251 Pure composition, without performance degradation 252 (composition happens before actual execution of SSZ serialization/hashing). 253 254 255 ### Composition example 256 257 ```go 258 package main 259 260 import ( 261 . "github.com/protolambda/zssz/types" 262 "reflect" 263 ) 264 265 // A simple interface to call when composing a SSZ for your type. 266 func MyFactory(typ reflect.Type) (SSZ, error) { 267 // Pass this same factory to it, this 268 return myFactory(MyFactory, typ) 269 } 270 271 // other factories may want to use this one for some of their functionality, make it public. 272 func MyFactoryFn(factory SSZFactoryFn, typ reflect.Type) (SSZ, error) { 273 // figure out which SSZ type definition to use, based on interface check 274 if typ.Implements(myFancyCustomTypeInterface) { 275 // pass it the typ, it's recommended to make the function check if the type is really allowed. 276 // This makes usage by other 3rd-party factories safe. 277 return FancyTypeSSZ(typ) 278 } 279 280 // figure out which SSZ type definition to use, based on kind 281 switch typ.Kind() { 282 case reflect.Struct: 283 // pass it a factory, to build SSZ definitions for child-types (container fields). 284 return MyContainerSSZDefinition(factory, typ) 285 default: 286 // use the default behavior otherwise 287 return DefaultSSZFactory(factory, typ) 288 } 289 } 290 291 func main() { 292 // building a SSZ type definition using a factory 293 myThingSSZ, _ := MySSZFactory(reflect.TypeOf((*MyThing)(nil)).Elem()) 294 } 295 ``` 296 297 ### Writing a custom SSZ type definition 298 299 Simply implement the `SSZ` interface. And provide some function to instantiate it (See `Vector` as example), 300 or maybe declare a singleton instance (See `ssz_basic` as example). 301 You can then mix it in however you like with the factory pattern, and start serializing/hashing your way. 302 303 304 ## Contact 305 306 Dev: [@protolambda on Twitter](https://twitter.com/protolambda) 307 308 309 ## License 310 311 MIT, see license file. 312