ssz
Summary
Simple Serialize (SSZ) is a consensus layer standard that defines how consensus objects are serialized and merkleized.
SSZ is a type system that defines:
- efficient serialization / deserialization
- stable merkleization
- default constructor
Additionally, this library allows for additional operations:
- create and consume proofs
- to / from json-serializable object
- copy / clone
Install
npm install @chainsafe/ssz
Usage
import {ContainerType, ByteVectorType} from "@chainsafe/ssz";
const Keypair = new ContainerType({
privateKey: new ByteVectorType(32),
publicKey: new ByteVectorType(48),
},
});
import {ValueOf} from "@chainsafe/ssz";
type Keypair = ValueOf<typeof Keypair>
const keypair = Keypair.defaultValue();
keypair.privateKey;
keypair.publicKey;
const serialized: Uint8Array = Keypair.serialize(keypair);
const root: Uint8Array = Keypair.hashTreeRoot(keypair);
const keypair2: Keypair = Keypair.clone(kp);
const keypair3: Keypair = Keypair.deserialize(serialized);
const keypairJSON = Keypair.toJson(keypair);
const keypairJSONStr = JSON.stringify(jsonKp);
const keypair4: Keypair = Keypair.fromJson(keypairJSON);
const keypairView: TreeBacked<Keypair> = Keypair.toView(keypair)
keypairView.serialize();
Ethereum consensus objects
For Ethereum consensus datatypes (eg: BeaconBlock
, DepositData
, BeaconState
, etc), see @chainsafe/lodestar-types
.
Additional notes
Backings
This library operates on values of two kinds of 'backings', or underlying representations of data. Each backing has runtime tradeoffs for the above operations that arise from the nature of the underlying representation.
Prior versions of this library attempted to fully align the interfaces between operations on various backings. This has the side effect of obscuring the underlying representation in downstream code, which is undesirable when maintaining code that requires higher levels control, eg: performance-critical code. This library no longer supports interchanging values with different backings, and exported APIs clearly distinguish between backings.
We support the following backings:
- Value - This backing has a native javascript type representation.
Containers are constructed as Javascript Objects, vectors and lists as Arrays (or TypedArrays). Type methods type.serialize
. type.deserialize
, type.hashTreeRoot
, and type.defaultValue
all operate on values.
- Tree - This backing has an immutable merkle tree representation.
The data is represented as a full merkle tree, composed of immutable, linked nodes. (See @chainsafe/persistent-merkle-tree
), wrapped as a "tree view". Two types of tree view are provided, a simple wrapper, and a wrapper with more caching and batched updates.
Tree View
A tree view is a wrapper around a Tree
and a Type
that provides methods for convenient property access and ssz operations.
Property getters return sub-views, except for basic types, which return native values. Setters, likewise, require sub-views, except for basic types, which require native views.
This tree view is a simple wrapper to tree backed data that commits any changes immediately to the tree. Changes are propagated upwards to the root parent tree.
const C = new ContainerType({
a: new VectorBasicType(new UintNumberType(1), 2),
});
const c = C.defaultView();
c.serialize() === C.hashTreeRoot(C.defaultValue());
const root = c.hashTreeRoot();
c.a.get(0) === 0;
c.a.set(0, 1);
assert(root.toString() !== c.hashTreeRoot().toString());
If you need to do many mutations at once see ViewDU, which defers all updates to a later commit
step, paying the cost of updating the tree only once.
Subview behaviour
View implementations don't contain any internal caches beyond their internal Tree
s, and setting one subview to another will not link the views.
const c1 = C.toView({a: [0, 0]});
const c2 = C.toView({a: [1, 1]});
c1.a = c2.a;
c1.a.set(0, 2);
c2.a.set(0, 3);
Tree ViewDU
ViewDU = View Deferred Update. This tree view caches all mutations to data and applies the changes to the tree only when requested by calling the commit
method. This allows to pay to cost of navigating and updating the tree only once. This strategy is optimal for large tree manipulations that require very high performance (i.e. the Ethereum consensus beacon chain state transition).
const C = new ContainerType({
a: new VectorBasicType(new UintNumberType(1), 2),
});
const c = C.defaultViewDU();
c.serialize() === C.hashTreeRoot(C.defaultValue());
const root = c.hashTreeRoot();
c.a.get(0) === 0;
c.a.set(0, 1);
assert(root.toString() === c.hashTreeRoot().toString());
c.commit();
assert(root.toString() !== c.hashTreeRoot().toString());
Key features
- Defer tree updates until
commit
is called, allowing multiple nodes to tree to be set in a batch and navigating through the tree at most once - Persist caches of sub-properties to prevent tree navigation when re-reading data.
Subview behaviour
Due to having mutable caches for children properties, setting a subview from one view to the subview of another view will link the two by referencing the same underlying cache.
const c1 = C.toViewDU({a: [0, 0]});
const c2 = C.toViewDU({a: [1, 1]});
c1.a = c2.a;
c1.a.set(0, 2);
c2.a.set(0, 3);
License
Apache 2.0