Skip to content

Commit

Permalink
feat: use @chainsafe/blst directly (#6706)
Browse files Browse the repository at this point in the history
* feat: use @chainsafe/blst directly

* chore: update to blst@1.0.1

* refactor: remove randomBytesNonZero and user blst exported version

* chore: update blst references

* test: catch invalid deserialization in spec tests and return false

* feat: create signatureFromBytes and signatureFromBytesNoCheck in utils package

* feat: implement signatureFromBytes from utils package

* feat: implement signatureFromBytes everywhere

* fix: light-client empty module for blst

---------

Co-authored-by: matthewkeil <me@matthewkeil.com>
  • Loading branch information
wemeetagain and matthewkeil authored May 24, 2024
1 parent 794b9f1 commit 66fe753
Show file tree
Hide file tree
Showing 97 changed files with 394 additions and 355 deletions.
3 changes: 1 addition & 2 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@
},
"dependencies": {
"@chainsafe/as-sha256": "^0.4.1",
"@chainsafe/bls": "^8.1.0",
"@chainsafe/blst": "^1.0.0",
"@chainsafe/blst": "^1.0.1",
"@chainsafe/discv5": "^9.0.0",
"@chainsafe/enr": "^3.0.0",
"@chainsafe/libp2p-gossipsub": "^13.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/bls/interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PublicKey} from "@chainsafe/bls/types";
import {PublicKey} from "@chainsafe/blst";
import {ISignatureSet} from "@lodestar/state-transition";

export type VerifySignatureOpts = {
Expand Down
15 changes: 6 additions & 9 deletions packages/beacon-node/src/chain/bls/jobItem.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import bls from "@chainsafe/bls";
import {CoordType, PointFormat, PublicKey} from "@chainsafe/bls/types";
import {PublicKey, aggregatePublicKeys, aggregateSignatures, randomBytesNonZero} from "@chainsafe/blst";
import {signatureFromBytes} from "@lodestar/utils";
import {ISignatureSet, SignatureSetType} from "@lodestar/state-transition";
import {LinkedList} from "../../util/array.js";
import {Metrics} from "../../metrics/metrics.js";
import {VerifySignatureOpts} from "./interface.js";
import {getAggregatedPubkey} from "./utils.js";
import {BlsWorkReq} from "./types.js";
import {randomBytesNonZero} from "./utils.js";

export type JobQueueItem = JobQueueItemDefault | JobQueueItemSameMessage;

Expand Down Expand Up @@ -50,7 +49,7 @@ export function jobItemSigSets(job: JobQueueItem): number {
* Prepare BlsWorkReq from JobQueueItem
* WARNING: May throw with untrusted user input
*/
export function jobItemWorkReq(job: JobQueueItem, format: PointFormat, metrics: Metrics | null): BlsWorkReq {
export function jobItemWorkReq(job: JobQueueItem, metrics: Metrics | null): BlsWorkReq {
switch (job.type) {
case JobQueueItemType.default:
return {
Expand All @@ -71,11 +70,9 @@ export function jobItemWorkReq(job: JobQueueItem, format: PointFormat, metrics:
// and not a problem in the near future
// this is monitored on v1.11.0 https://github.com/ChainSafe/lodestar/pull/5912#issuecomment-1700320307
const timer = metrics?.blsThreadPool.signatureDeserializationMainThreadDuration.startTimer();
const signatures = job.sets.map((set) => bls.Signature.fromBytes(set.signature, CoordType.affine, true));
const signatures = job.sets.map((set) => signatureFromBytes(set.signature));
timer?.();

// adding verification randomness is napi specific. must not attempt with herumi until
// @chainsafe/bls is updated to support it with herumi
const randomness: Uint8Array[] = [];
for (let i = 0; i < job.sets.length; i++) {
randomness.push(randomBytesNonZero(8));
Expand All @@ -85,8 +82,8 @@ export function jobItemWorkReq(job: JobQueueItem, format: PointFormat, metrics:
sets: [
{
message: job.message,
publicKey: bls.PublicKey.aggregate(job.sets.map((set, i) => set.publicKey.multiplyBy(randomness[i]))),
signature: bls.Signature.aggregate(signatures.map((sig, i) => sig.multiplyBy(randomness[i]))),
publicKey: aggregatePublicKeys(job.sets.map((set, i) => set.publicKey.multiplyBy(randomness[i]))),
signature: aggregateSignatures(signatures.map((sig, i) => sig.multiplyBy(randomness[i]))),
},
],
};
Expand Down
11 changes: 2 additions & 9 deletions packages/beacon-node/src/chain/bls/multiThread.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os from "node:os";
import bls from "@chainsafe/bls";
import {PointFormat, PublicKey} from "@chainsafe/bls/types";
import {PublicKey} from "@chainsafe/blst";
import {Logger} from "@lodestar/utils";
import {ISignatureSet} from "@lodestar/state-transition";
import {QueueError, QueueErrorCode} from "../../util/queue/index.js";
Expand Down Expand Up @@ -76,8 +75,6 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier {
private readonly logger: Logger;
private readonly metrics: Metrics | null;

private readonly format = PointFormat.uncompressed;

private blsPoolSize: number;
private workersBusy = 0;

Expand All @@ -93,10 +90,6 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier {
private closed = false;

constructor(options: BlsMultiThreadWorkerPoolOptions, modules: BlsMultiThreadWorkerPoolModules) {
if (bls.implementation === "herumi") {
throw new Error("Herumi BLS implementation is not supported");
}

this.logger = modules.logger;
this.blsVerifyAllMultiThread = options.blsVerifyAllMultiThread ?? false;

Expand Down Expand Up @@ -298,7 +291,7 @@ export class BlsMultiThreadWorkerPool implements IBlsVerifier {
try {
// Note: This can throw, must be handled per-job.
// Pubkey and signature aggregation is defered here
workReq = jobItemWorkReq(job, this.format, this.metrics);
workReq = jobItemWorkReq(job, this.metrics);
} catch (e) {
this.metrics?.blsThreadPool.errorAggregateSignatureSetsCount.inc({type: job.type});

Expand Down
15 changes: 7 additions & 8 deletions packages/beacon-node/src/chain/bls/singleThread.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {PublicKey, Signature} from "@chainsafe/bls/types";
import bls from "@chainsafe/bls";
import {CoordType} from "@chainsafe/bls/types";
import {PublicKey, Signature, aggregatePublicKeys, aggregateSignatures, verify} from "@chainsafe/blst";
import {ISignatureSet} from "@lodestar/state-transition";
import {signatureFromBytes} from "@lodestar/utils";
import {Metrics} from "../../metrics/index.js";
import {IBlsVerifier} from "./interface.js";
import {verifySets} from "./verifySets.js";
Expand Down Expand Up @@ -40,12 +39,12 @@ export class BlsSingleThreadVerifier implements IBlsVerifier {
message: Uint8Array
): Promise<boolean[]> {
const timer = this.metrics?.blsThreadPool.mainThreadDurationInThreadPool.startTimer();
const pubkey = bls.PublicKey.aggregate(sets.map((set) => set.publicKey));
const pubkey = aggregatePublicKeys(sets.map((set) => set.publicKey));
let isAllValid = true;
// validate signature = true
const signatures = sets.map((set) => {
try {
return bls.Signature.fromBytes(set.signature, CoordType.affine, true);
return signatureFromBytes(set.signature);
} catch (_) {
// at least one set has malformed signature
isAllValid = false;
Expand All @@ -54,8 +53,8 @@ export class BlsSingleThreadVerifier implements IBlsVerifier {
});

if (isAllValid) {
const signature = bls.Signature.aggregate(signatures as Signature[]);
isAllValid = signature.verify(pubkey, message);
const signature = aggregateSignatures(signatures as Signature[]);
isAllValid = verify(message, pubkey, signature);
}

let result: boolean[];
Expand All @@ -67,7 +66,7 @@ export class BlsSingleThreadVerifier implements IBlsVerifier {
if (sig === null) {
return false;
}
return sig.verify(set.publicKey, message);
return verify(message, set.publicKey, sig);
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/bls/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PublicKey, Signature} from "@chainsafe/bls/types";
import {PublicKey, Signature} from "@chainsafe/blst";
import {VerifySignatureOpts} from "./interface.js";

export type DeserializedKeySet = {
Expand Down
18 changes: 2 additions & 16 deletions packages/beacon-node/src/chain/bls/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import bls from "@chainsafe/bls";
import {PublicKey} from "@chainsafe/bls/types";
import {PublicKey, aggregatePublicKeys} from "@chainsafe/blst";
import {ISignatureSet, SignatureSetType} from "@lodestar/state-transition";
import {Metrics} from "../../metrics/metrics.js";
import {WorkResultError} from "./types.js";
Expand All @@ -11,7 +10,7 @@ export function getAggregatedPubkey(signatureSet: ISignatureSet, metrics: Metric

case SignatureSetType.aggregate: {
const timer = metrics?.blsThreadPool.pubkeysAggregationMainThreadDuration.startTimer();
const pubkeys = bls.PublicKey.aggregate(signatureSet.pubkeys);
const pubkeys = aggregatePublicKeys(signatureSet.pubkeys);
timer?.();
return pubkeys;
}
Expand Down Expand Up @@ -51,19 +50,6 @@ export function chunkifyMaximizeChunkSize<T>(arr: T[], minPerChunk: number): T[]
return arrArr;
}

/**
* `rand` must not be exactly zero. Otherwise it would allow the verification of invalid signatures
* See https://github.com/ChainSafe/blst-ts/issues/45
*/
export function randomBytesNonZero(bytesCount: number): Uint8Array {
const rand = crypto.getRandomValues(new Uint8Array(bytesCount));
for (let i = 0; i < bytesCount; i++) {
if (rand[i] !== 0) return rand;
}
rand[0] = 1;
return rand;
}

export function getJobResultError(jobResult: WorkResultError | null, i: number): Error {
const workerError = jobResult ? Error(jobResult.error.message) : Error(`No jobResult for index ${i}`);
if (jobResult?.error?.stack) workerError.stack = jobResult.error.stack;
Expand Down
15 changes: 10 additions & 5 deletions packages/beacon-node/src/chain/bls/verifySets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import bls from "@chainsafe/bls";
import {
asyncVerify,
asyncVerifyMultipleAggregateSignatures,
verify,
verifyMultipleAggregateSignatures,
} from "@chainsafe/blst";
import {WorkRequestSet} from "./types.js";

const MIN_SET_COUNT_TO_BATCH = 2;
Expand All @@ -10,7 +15,7 @@ const MIN_SET_COUNT_TO_BATCH = 2;
export function verifySets(sets: WorkRequestSet[]): boolean {
try {
if (sets.length >= MIN_SET_COUNT_TO_BATCH) {
return bls.verifyMultipleSignatures(sets);
return verifyMultipleAggregateSignatures(sets);
}

// .every on an empty array returns true
Expand All @@ -19,7 +24,7 @@ export function verifySets(sets: WorkRequestSet[]): boolean {
}

// If too few signature sets verify them without batching
return sets.every(({message, publicKey, signature}) => bls.verify(publicKey, message, signature));
return sets.every(({message, publicKey, signature}) => verify(message, publicKey, signature));
} catch (_) {
// A signature could be malformed, in that case fromBytes throws error
// blst-ts `verifyMultipleSignatures` is also a fallible operation if mul_n_aggregate fails
Expand All @@ -31,7 +36,7 @@ export function verifySets(sets: WorkRequestSet[]): boolean {
export async function asyncVerifySets(sets: WorkRequestSet[]): Promise<boolean> {
try {
if (sets.length >= MIN_SET_COUNT_TO_BATCH) {
return await bls.asyncVerifyMultipleSignatures(sets);
return await asyncVerifyMultipleAggregateSignatures(sets);
}

// .every on an empty array returns true
Expand All @@ -40,7 +45,7 @@ export async function asyncVerifySets(sets: WorkRequestSet[]): Promise<boolean>
}

const promises = await Promise.all(
sets.map(({message, publicKey, signature}) => bls.asyncVerify(publicKey, message, signature))
sets.map(({message, publicKey, signature}) => asyncVerify(message, publicKey, signature))
);
// If too few signature sets verify them without batching
return promises.every((isValid) => isValid);
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ export class BeaconChain implements IBeaconChain {
RegenCaller.produceBlock
);
const proposerIndex = state.epochCtx.getBeaconProposer(slot);
const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes();
const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].serialize();

const {body, blobs, executionPayloadValue, shouldOverrideBuilder} = await produceBlockBody.call(
this,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import bls from "@chainsafe/bls";
import {toHexString} from "@chainsafe/ssz";
import {aggregateSignatures} from "@chainsafe/blst";
import {ForkName, ForkSeq, MAX_ATTESTATIONS, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params";
import {phase0, Epoch, Slot, ssz, ValidatorIndex, RootHex} from "@lodestar/types";
import {
Expand All @@ -11,9 +11,9 @@ import {
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import {IForkChoice, EpochDifference} from "@lodestar/fork-choice";
import {toHex, MapDef} from "@lodestar/utils";
import {toHex, MapDef, signatureFromBytesNoCheck} from "@lodestar/utils";
import {intersectUint8Arrays, IntersectResult} from "../../util/bitArray.js";
import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js";
import {pruneBySlot} from "./utils.js";
import {InsertOutcome} from "./types.js";

type DataRootHex = string;
Expand Down Expand Up @@ -383,7 +383,7 @@ export function aggregateInto(attestation1: AttestationWithIndex, attestation2:

const signature1 = signatureFromBytesNoCheck(attestation1.attestation.signature);
const signature2 = signatureFromBytesNoCheck(attestation2.attestation.signature);
attestation1.attestation.signature = bls.Signature.aggregate([signature1, signature2]).toBytes();
attestation1.attestation.signature = aggregateSignatures([signature1, signature2]).serialize();
}

/**
Expand Down
14 changes: 5 additions & 9 deletions packages/beacon-node/src/chain/opPools/attestationPool.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {PointFormat, Signature} from "@chainsafe/bls/types";
import bls from "@chainsafe/bls";
import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray} from "@chainsafe/ssz";
import {phase0, Slot, RootHex} from "@lodestar/types";
import {MapDef} from "@lodestar/utils";
import {MapDef, signatureFromBytesNoCheck} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js";
import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js";
import {pruneBySlot} from "./utils.js";

/**
* The number of slots that will be stored in the pool.
Expand Down Expand Up @@ -191,10 +190,7 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: phase0.
}

aggregate.aggregationBits.set(bitIndex, true);
aggregate.signature = bls.Signature.aggregate([
aggregate.signature,
signatureFromBytesNoCheck(attestation.signature),
]);
aggregate.signature = aggregateSignatures([aggregate.signature, signatureFromBytesNoCheck(attestation.signature)]);
return InsertOutcome.Aggregated;
}

Expand All @@ -217,6 +213,6 @@ function fastToAttestation(aggFast: AggregateFast): phase0.Attestation {
return {
data: aggFast.data,
aggregationBits: aggFast.aggregationBits,
signature: aggFast.signature.toBytes(PointFormat.compressed),
signature: aggFast.signature.serialize(true),
};
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {PointFormat, Signature} from "@chainsafe/bls/types";
import bls from "@chainsafe/bls";
import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray, toHexString} from "@chainsafe/ssz";
import {SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params";
import {altair, Root, Slot, SubcommitteeIndex} from "@lodestar/types";
import {MapDef} from "@lodestar/utils";
import {MapDef, signatureFromBytesNoCheck} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js";
import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js";
import {pruneBySlot} from "./utils.js";

/**
* SyncCommittee signatures are only useful during a single slot according to our peer's clocks
Expand Down Expand Up @@ -108,7 +107,7 @@ export class SyncCommitteeMessagePool {
return {
...contribution,
aggregationBits: contribution.aggregationBits,
signature: contribution.signature.toBytes(PointFormat.compressed),
signature: contribution.signature.serialize(true),
};
}

Expand Down Expand Up @@ -136,7 +135,7 @@ function aggregateSignatureInto(
}

contribution.aggregationBits.set(indexInSubcommittee, true);
contribution.signature = bls.Signature.aggregate([
contribution.signature = aggregateSignatures([
contribution.signature,
signatureFromBytesNoCheck(signature.signature),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type {Signature} from "@chainsafe/bls/types";
import bls from "@chainsafe/bls";
import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray, toHexString} from "@chainsafe/ssz";
import {SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params";
import {altair, Slot, Root, ssz} from "@lodestar/types";
import {G2_POINT_AT_INFINITY} from "@lodestar/state-transition";
import {MapDef} from "@lodestar/utils";
import {MapDef, signatureFromBytesNoCheck} from "@lodestar/utils";
import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js";
import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js";
import {pruneBySlot} from "./utils.js";

/**
* SyncCommittee aggregates are only useful for the next block they have signed.
Expand Down Expand Up @@ -182,6 +181,6 @@ export function aggregate(bestContributionBySubnet: Map<number, SyncContribution
}
return {
syncCommitteeBits,
syncCommitteeSignature: bls.Signature.aggregate(signatures).toBytes(),
syncCommitteeSignature: aggregateSignatures(signatures).serialize(),
};
}
10 changes: 0 additions & 10 deletions packages/beacon-node/src/chain/opPools/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import bls from "@chainsafe/bls";
import {CoordType, Signature} from "@chainsafe/bls/types";
import {BLS_WITHDRAWAL_PREFIX} from "@lodestar/params";
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {Slot, capella} from "@lodestar/types";
Expand All @@ -25,14 +23,6 @@ export function pruneBySlot(map: Map<Slot, unknown>, slot: Slot, slotsRetained:
return lowestPermissibleSlot;
}

/**
* De-serialize bytes into Signature.
* No need to verify Signature is valid, already run sig-verify = false
*/
export function signatureFromBytesNoCheck(signature: Uint8Array): Signature {
return bls.Signature.fromBytes(signature, CoordType.affine, false);
}

/**
* Ensures that a SignedBLSToExecutionChange object is _still_ valid for block inclusion. An object valid for the pool,
* can become invalid for certain forks.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {PublicKey} from "@chainsafe/bls/types";
import {PublicKey} from "@chainsafe/blst";
import {DOMAIN_AGGREGATE_AND_PROOF} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {Epoch, phase0} from "@lodestar/types";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {PublicKey} from "@chainsafe/bls/types";
import {PublicKey} from "@chainsafe/blst";
import {DOMAIN_SELECTION_PROOF} from "@lodestar/params";
import {phase0, Slot, ssz} from "@lodestar/types";
import {computeSigningRoot, createSingleSignatureSetFromComponents, ISignatureSet} from "@lodestar/state-transition";
Expand Down
Loading

0 comments on commit 66fe753

Please sign in to comment.