-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement ValidatorViewDU class
- Loading branch information
Showing
5 changed files
with
449 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import {ByteViews} from "../../../src/type/abstract"; | ||
import {ContainerNodeStructType} from "../../../src/type/containerNodeStruct"; | ||
import {ValueOfFields} from "../../../src/view/container"; | ||
import * as primitiveSsz from "../primitive/sszTypes"; | ||
import { ValidatorTreeViewDU } from "./viewDU/validatorNodeStruct"; | ||
import {Node} from "@chainsafe/persistent-merkle-tree"; | ||
|
||
const {Boolean, Bytes32, UintNum64, BLSPubkey, EpochInf} = primitiveSsz; | ||
|
||
// this is to work with uint32, see https://github.com/ChainSafe/ssz/blob/ssz-v0.15.1/packages/ssz/src/type/uint.ts | ||
const NUMBER_2_POW_32 = 2 ** 32; | ||
|
||
/* | ||
* Below constants are respective to their ssz type in `ValidatorType`. | ||
*/ | ||
const UINT32_SIZE = 4; | ||
const PUBKEY_SIZE = 48; | ||
const WITHDRAWAL_CREDENTIALS_SIZE = 32; | ||
const SLASHED_SIZE = 1; | ||
|
||
export const ValidatorType = { | ||
pubkey: BLSPubkey, | ||
withdrawalCredentials: Bytes32, | ||
effectiveBalance: UintNum64, | ||
slashed: Boolean, | ||
activationEligibilityEpoch: EpochInf, | ||
activationEpoch: EpochInf, | ||
exitEpoch: EpochInf, | ||
withdrawableEpoch: EpochInf, | ||
}; | ||
|
||
/** | ||
* Improve serialization performance for state.validators.serialize(); | ||
*/ | ||
export class ValidatorNodeStructType extends ContainerNodeStructType<typeof ValidatorType> { | ||
constructor() { | ||
super(ValidatorType, {typeName: "Validator", jsonCase: "eth2"}); | ||
} | ||
|
||
getViewDU(node: Node): ValidatorTreeViewDU { | ||
return new ValidatorTreeViewDU(this, node); | ||
} | ||
|
||
value_serializeToBytes( | ||
{uint8Array: output, dataView}: ByteViews, | ||
offset: number, | ||
validator: ValueOfFields<typeof ValidatorType> | ||
): number { | ||
output.set(validator.pubkey, offset); | ||
offset += PUBKEY_SIZE; | ||
output.set(validator.withdrawalCredentials, offset); | ||
offset += WITHDRAWAL_CREDENTIALS_SIZE; | ||
const {effectiveBalance, activationEligibilityEpoch, activationEpoch, exitEpoch, withdrawableEpoch} = validator; | ||
// effectiveBalance is UintNum64 | ||
dataView.setUint32(offset, effectiveBalance & 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
dataView.setUint32(offset, (effectiveBalance / NUMBER_2_POW_32) & 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
output[offset] = validator.slashed ? 1 : 0; | ||
offset += SLASHED_SIZE; | ||
offset = writeEpochInf(dataView, offset, activationEligibilityEpoch); | ||
offset = writeEpochInf(dataView, offset, activationEpoch); | ||
offset = writeEpochInf(dataView, offset, exitEpoch); | ||
offset = writeEpochInf(dataView, offset, withdrawableEpoch); | ||
|
||
return offset; | ||
} | ||
} | ||
|
||
function writeEpochInf(dataView: DataView, offset: number, value: number): number { | ||
if (value === Infinity) { | ||
dataView.setUint32(offset, 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
dataView.setUint32(offset, 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
} else { | ||
dataView.setUint32(offset, value & 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
dataView.setUint32(offset, (value / NUMBER_2_POW_32) & 0xffffffff, true); | ||
offset += UINT32_SIZE; | ||
} | ||
return offset; | ||
} | ||
export const ValidatorNodeStruct = new ValidatorNodeStructType(); |
251 changes: 251 additions & 0 deletions
251
packages/ssz/test/lodestarTypes/phase0/viewDU/validatorNodeStruct.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
import { HashObject, byteArrayToHashObject } from "@chainsafe/as-sha256"; | ||
import { BranchNodeStruct } from "../../../../src/branchNodeStruct"; | ||
import { ContainerTypeGeneric } from "../../../../src/view/container"; | ||
import { TreeViewDU } from "../../../../src/viewDU/abstract"; | ||
import { ValidatorType } from "../validator"; | ||
import { | ||
Node, | ||
BranchNode, | ||
HashComputationGroup, | ||
} from "@chainsafe/persistent-merkle-tree"; | ||
type Validator = { | ||
pubkey: Uint8Array; | ||
withdrawalCredentials: Uint8Array; | ||
effectiveBalance: number; | ||
slashed: boolean; | ||
activationEligibilityEpoch: number; | ||
activationEpoch: number; | ||
exitEpoch: number; | ||
withdrawableEpoch: number; | ||
}; | ||
|
||
const numFields = 8; | ||
const NUMBER_2_POW_32 = 2 ** 32; | ||
|
||
/** | ||
* A specific ViewDU for validator designed to be efficient to batch hash and efficient to create tree | ||
* because it uses prepopulated nodes to do that. | ||
*/ | ||
export class ValidatorTreeViewDU extends TreeViewDU<ContainerTypeGeneric<typeof ValidatorType>> { | ||
protected valueChanged: Validator | null = null; | ||
protected _rootNode: BranchNodeStruct<Validator>; | ||
|
||
constructor(readonly type: ContainerTypeGeneric<typeof ValidatorType>, node: Node) { | ||
super(); | ||
this._rootNode = node as BranchNodeStruct<Validator>; | ||
} | ||
|
||
get node(): Node { | ||
return this._rootNode; | ||
} | ||
|
||
get cache(): void { | ||
return; | ||
} | ||
|
||
commit(hashComps: HashComputationGroup | null = null): void { | ||
if (this.valueChanged !== null) { | ||
// TODO - batch: throw error when testing, should be committed by parent | ||
const value = this.valueChanged; | ||
this.valueChanged = null; | ||
this._rootNode = this.type.value_toTree(value) as BranchNodeStruct<Validator>; | ||
} | ||
|
||
if (hashComps !== null) { | ||
this._rootNode.getHashComputations(hashComps); | ||
} | ||
} | ||
|
||
get pubkey(): Uint8Array { | ||
return (this.valueChanged || this._rootNode.value).pubkey; | ||
} | ||
|
||
set pubkey(value: Uint8Array) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.pubkey = value; | ||
} | ||
|
||
get withdrawalCredentials(): Uint8Array { | ||
return (this.valueChanged || this._rootNode.value).withdrawalCredentials; | ||
} | ||
|
||
set withdrawalCredentials(value: Uint8Array) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.withdrawalCredentials = value; | ||
} | ||
|
||
get effectiveBalance(): number { | ||
return (this.valueChanged || this._rootNode.value).effectiveBalance; | ||
} | ||
|
||
set effectiveBalance(value: number) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.effectiveBalance = value; | ||
} | ||
|
||
get slashed(): boolean { | ||
return (this.valueChanged || this._rootNode.value).slashed; | ||
} | ||
|
||
set slashed(value: boolean) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.slashed = value; | ||
} | ||
|
||
get activationEligibilityEpoch(): number { | ||
return (this.valueChanged || this._rootNode.value).activationEligibilityEpoch; | ||
} | ||
|
||
set activationEligibilityEpoch(value: number) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.activationEligibilityEpoch = value; | ||
} | ||
|
||
get activationEpoch(): number { | ||
return (this.valueChanged || this._rootNode.value).activationEpoch; | ||
} | ||
|
||
set activationEpoch(value: number) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.activationEpoch = value; | ||
} | ||
|
||
get exitEpoch(): number { | ||
return (this.valueChanged || this._rootNode.value).exitEpoch; | ||
} | ||
|
||
set exitEpoch(value: number) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.exitEpoch = value; | ||
} | ||
|
||
get withdrawableEpoch(): number { | ||
return (this.valueChanged || this._rootNode.value).withdrawableEpoch; | ||
} | ||
|
||
set withdrawableEpoch(value: number) { | ||
if (this.valueChanged === null) { | ||
this.valueChanged = this.type.clone(this._rootNode.value); | ||
} | ||
|
||
this.valueChanged.withdrawableEpoch = value; | ||
} | ||
|
||
/** | ||
* This is called by parent to populate nodes with valueChanged. | ||
* Parent will then hash the nodes to get the root hash, then call commitToHashObject to update the root hash. | ||
* Note that node[0] should be a branch node due to pubkey of 48 bytes. | ||
*/ | ||
valueToTree(nodes: Node[]): void { | ||
if (this.valueChanged === null) { | ||
return; | ||
} | ||
|
||
if (nodes.length !== numFields) { | ||
throw new Error(`Expected ${numFields} fields, got ${nodes.length}`); | ||
} | ||
|
||
validatorToTree(nodes, this.valueChanged); | ||
} | ||
|
||
/** | ||
* The HashObject is computed by parent so that we don't need to create a tree from scratch. | ||
*/ | ||
commitToHashObject(ho: HashObject): void { | ||
if (this.valueChanged === null) { | ||
return; | ||
} | ||
|
||
const oldRoot = this._rootNode; | ||
const value = this.valueChanged; | ||
this._rootNode = new BranchNodeStruct(oldRoot["valueToNode"], value); | ||
this._rootNode.applyHash(ho); | ||
this.valueChanged = null; | ||
} | ||
|
||
protected clearCache(): void { | ||
this.valueChanged = null; | ||
} | ||
|
||
get name(): string { | ||
return this.type.typeName; | ||
} | ||
} | ||
|
||
/** | ||
* Fast way to write value to tree. Input nodes are at level 3 as below: | ||
* level | ||
* 0 validator root | ||
* / \ | ||
* 1 10 11 | ||
* / \ / \ | ||
* 2 20 21 22 23 | ||
* / \ / \ / \ / \ | ||
* 3 pub with eff sla act act exit with | ||
* / \ | ||
* 4 pub0 pub1 | ||
* | ||
* After this function all nodes at level 4 and level 3 (except for pubkey) are populated | ||
* We can then use HashComputation to compute the root hash in batch at state.validators ViewDU | ||
* // TODO - batch: is it more performant to convert to Uint8Array and hash in batch? | ||
*/ | ||
export function validatorToTree(nodes: Node[], value: Validator): void { | ||
const { pubkey, withdrawalCredentials, effectiveBalance, slashed, activationEligibilityEpoch, activationEpoch, exitEpoch, withdrawableEpoch } = value; | ||
|
||
// pubkey 48 bytes = pub0 + pub1 | ||
const node0 = nodes[0]; | ||
if (node0.isLeaf()) { | ||
throw Error("Expected pubkeyNode to be a BranchNode"); | ||
} | ||
const pubkeyNode = node0 as BranchNode; | ||
pubkeyNode.left.applyHash(byteArrayToHashObject(pubkey.subarray(0, 32))); | ||
pubkeyNode.right.applyHash(byteArrayToHashObject(pubkey.subarray(32, 48))); | ||
// withdrawalCredentials | ||
nodes[1].applyHash(byteArrayToHashObject(withdrawalCredentials)); | ||
// effectiveBalance, 8 bytes = h0 + h1 | ||
writeEpochInfToNode(effectiveBalance, nodes[2]); | ||
// slashed | ||
nodes[3].h0 = slashed ? 1 : 0; | ||
// activationEligibilityEpoch, 8 bytes = h0 + h1 | ||
writeEpochInfToNode(activationEligibilityEpoch, nodes[4]); | ||
// activationEpoch, 8 bytes = h0 + h1 | ||
writeEpochInfToNode(activationEpoch, nodes[5]); | ||
// exitEpoch, 8 bytes = h0 + h1 | ||
writeEpochInfToNode(exitEpoch, nodes[6]); | ||
// withdrawableEpoch, 8 bytes = h0 + h1 | ||
writeEpochInfToNode(withdrawableEpoch, nodes[7]); | ||
} | ||
|
||
/** | ||
* An epoch is a 64-bit number, split into two 32-bit numbers and populate to h0 and h1. | ||
*/ | ||
function writeEpochInfToNode(epoch: number, node: Node): void { | ||
if (epoch === Infinity) { | ||
node.h0 = 0xffffffff; | ||
node.h1 = 0xffffffff; | ||
} else { | ||
node.h0 = epoch & 0xffffffff; | ||
node.h1 = (epoch / NUMBER_2_POW_32) & 0xffffffff; | ||
} | ||
} |
Oops, something went wrong.