From 6f1e52ca015a45b6443b16bb3de9280159a31a64 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 18 Nov 2023 14:23:44 +0700 Subject: [PATCH 1/9] fix: use 8 bytes as numbers in AttesterSlashing --- packages/beacon-node/src/network/network.ts | 5 +- .../chain/validation/attesterSlashing.test.ts | 5 +- packages/flare/src/cmds/selfSlashAttester.ts | 36 +++++----- .../src/block/isValidIndexedAttestation.ts | 8 +-- .../src/block/processAttesterSlashing.ts | 4 +- .../src/signatureSets/attesterSlashings.ts | 12 ++-- .../state-transition/src/util/attestation.ts | 13 ++-- .../state-transition/test/perf/block/util.ts | 17 +++-- .../unit/signatureSets/signatureSets.test.ts | 19 ++--- .../test/unit/util/slashing.test.ts | 9 +-- packages/types/package.json | 3 + packages/types/src/phase0/sszTypes.ts | 27 +++---- packages/types/src/phase0/types.ts | 4 +- .../types/test/unit/phase0/sszTypes.test.ts | 70 +++++++++++++++++++ packages/types/test/unit/ssz.test.ts | 8 +-- packages/utils/src/bytes.ts | 17 +++++ packages/utils/test/unit/bytes.test.ts | 20 +++++- 17 files changed, 200 insertions(+), 77 deletions(-) create mode 100644 packages/types/test/unit/phase0/sszTypes.test.ts diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 834ebacaa7a8..1463fef9180b 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -2,7 +2,7 @@ import {PeerId} from "@libp2p/interface/peer-id"; import {PublishOpts} from "@chainsafe/libp2p-gossipsub/types"; import {PeerScoreStatsDump} from "@chainsafe/libp2p-gossipsub/score"; import {BeaconConfig} from "@lodestar/config"; -import {sleep} from "@lodestar/utils"; +import {bytesToInt, sleep} from "@lodestar/utils"; import {LoggerNode} from "@lodestar/logger/node"; import {computeStartSlotAtEpoch, computeTimeAtSlot} from "@lodestar/state-transition"; import {phase0, allForks, deneb, altair, Root, capella, SlotRootHex} from "@lodestar/types"; @@ -351,7 +351,8 @@ export class Network implements INetwork { } async publishAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): Promise { - const fork = this.config.getForkName(Number(attesterSlashing.attestation1.data.slot as bigint)); + const slot = attesterSlashing.attestation1.data.slot; + const fork = this.config.getForkName(bytesToInt(slot)); return this.publishGossip( {type: GossipType.attester_slashing, fork}, attesterSlashing diff --git a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts index dcb07e5998ec..b3ed012d79fb 100644 --- a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts @@ -1,5 +1,6 @@ import {describe, it, beforeEach, afterEach, vi} from "vitest"; import {phase0, ssz} from "@lodestar/types"; +import {intToBytes} from "@lodestar/utils"; import {generateCachedState} from "../../../utils/state.js"; import {validateGossipAttesterSlashing} from "../../../../src/chain/validation/attesterSlashing.js"; import {AttesterSlashingErrorCode} from "../../../../src/chain/errors/attesterSlashingError.js"; @@ -44,7 +45,7 @@ describe("GossipMessageValidator", () => { }); it("should return valid attester slashing", async () => { - const attestationData = ssz.phase0.AttestationDataBigint.defaultValue(); + const attestationData = ssz.phase0.AttestationDataBytes8.defaultValue(); const attesterSlashing: phase0.AttesterSlashing = { attestation1: { data: attestationData, @@ -52,7 +53,7 @@ describe("GossipMessageValidator", () => { attestingIndices: [0], }, attestation2: { - data: {...attestationData, slot: BigInt(1)}, // Make it different so it's slashable + data: {...attestationData, slot: intToBytes(1, 8)}, // Make it different so it's slashable signature: Buffer.alloc(96, 0), attestingIndices: [0], }, diff --git a/packages/flare/src/cmds/selfSlashAttester.ts b/packages/flare/src/cmds/selfSlashAttester.ts index 3fa3414f5012..89fb1f71d8b6 100644 --- a/packages/flare/src/cmds/selfSlashAttester.ts +++ b/packages/flare/src/cmds/selfSlashAttester.ts @@ -5,7 +5,7 @@ import {phase0, ssz} from "@lodestar/types"; import {config as chainConfig} from "@lodestar/config/default"; import {createBeaconConfig, BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_ATTESTER, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; -import {toHexString} from "@lodestar/utils"; +import {bytesToInt, intToBytes, toHexString} from "@lodestar/utils"; import {computeSigningRoot} from "@lodestar/state-transition"; import {CliCommand} from "../util/command.js"; import {deriveSecretKeys, SecretKeysArgs, secretKeysOptions} from "../util/deriveSecretKeys.js"; @@ -51,7 +51,7 @@ export const selfSlashAttester: CliCommand, export async function selfSlashAttesterHandler(args: SelfSlashArgs): Promise { const sksAll = deriveSecretKeys(args); - const slot = BigInt(args.slot); // Throws if not valid + const slot = Number(args.slot); // Throws if not valid const batchSize = parseInt(args.batchSize); if (isNaN(batchSize)) throw Error(`Invalid arg batchSize ${args.batchSize}`); @@ -107,31 +107,31 @@ export async function selfSlashAttesterHandler(args: SelfSlashArgs): Promise sk.sign(signingRoot)); return bls.Signature.aggregate(sigs).toBytes(); diff --git a/packages/state-transition/src/block/isValidIndexedAttestation.ts b/packages/state-transition/src/block/isValidIndexedAttestation.ts index e3965b97ee73..635f3b62f4b8 100644 --- a/packages/state-transition/src/block/isValidIndexedAttestation.ts +++ b/packages/state-transition/src/block/isValidIndexedAttestation.ts @@ -2,7 +2,7 @@ import {MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; import {phase0} from "@lodestar/types"; import {CachedBeaconStateAllForks} from "../types.js"; import {verifySignatureSet} from "../util/index.js"; -import {getIndexedAttestationBigintSignatureSet, getIndexedAttestationSignatureSet} from "../signatureSets/index.js"; +import {getIndexedAttestationBytes8SignatureSet, getIndexedAttestationSignatureSet} from "../signatureSets/index.js"; /** * Check if `indexedAttestation` has sorted and unique indices and a valid aggregate signature. @@ -23,9 +23,9 @@ export function isValidIndexedAttestation( } } -export function isValidIndexedAttestationBigint( +export function isValidIndexedAttestationBytes8( state: CachedBeaconStateAllForks, - indexedAttestation: phase0.IndexedAttestationBigint, + indexedAttestation: phase0.IndexedAttestationBytes8, verifySignature: boolean ): boolean { if (!isValidIndexedAttestationIndices(state, indexedAttestation.attestingIndices)) { @@ -33,7 +33,7 @@ export function isValidIndexedAttestationBigint( } if (verifySignature) { - return verifySignatureSet(getIndexedAttestationBigintSignatureSet(state, indexedAttestation)); + return verifySignatureSet(getIndexedAttestationBytes8SignatureSet(state, indexedAttestation)); } else { return true; } diff --git a/packages/state-transition/src/block/processAttesterSlashing.ts b/packages/state-transition/src/block/processAttesterSlashing.ts index 0f635e33fce2..425c588ae935 100644 --- a/packages/state-transition/src/block/processAttesterSlashing.ts +++ b/packages/state-transition/src/block/processAttesterSlashing.ts @@ -4,7 +4,7 @@ import {ForkSeq} from "@lodestar/params"; import {isSlashableValidator, isSlashableAttestationData, getAttesterSlashableIndices} from "../util/index.js"; import {CachedBeaconStateAllForks} from "../types.js"; import {slashValidator} from "./slashValidator.js"; -import {isValidIndexedAttestationBigint} from "./isValidIndexedAttestation.js"; +import {isValidIndexedAttestationBytes8} from "./isValidIndexedAttestation.js"; /** * Process an AttesterSlashing operation. Initiates the exit of a validator, decreases the balance of the slashed @@ -53,7 +53,7 @@ export function assertValidAttesterSlashing( // be higher than the clock and the slashing would still be valid. Same applies to attestation data index, which // can be any arbitrary value. Must use bigint variants to hash correctly to all possible values for (const [i, attestation] of [attestation1, attestation2].entries()) { - if (!isValidIndexedAttestationBigint(state, attestation, verifySignatures)) { + if (!isValidIndexedAttestationBytes8(state, attestation, verifySignatures)) { throw new Error(`AttesterSlashing attestation${i} is invalid`); } } diff --git a/packages/state-transition/src/signatureSets/attesterSlashings.ts b/packages/state-transition/src/signatureSets/attesterSlashings.ts index 370d48a7d2f0..f2cc936277f5 100644 --- a/packages/state-transition/src/signatureSets/attesterSlashings.ts +++ b/packages/state-transition/src/signatureSets/attesterSlashings.ts @@ -2,6 +2,7 @@ import {allForks, phase0, ssz} from "@lodestar/types"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import {computeSigningRoot, computeStartSlotAtEpoch, ISignatureSet, SignatureSetType} from "../util/index.js"; import {CachedBeaconStateAllForks} from "../types.js"; +import {bytesToInt} from "@lodestar/utils"; /** Get signature sets from all AttesterSlashing objects in a block */ export function getAttesterSlashingsSignatureSets( @@ -19,22 +20,23 @@ export function getAttesterSlashingSignatureSets( attesterSlashing: phase0.AttesterSlashing ): ISignatureSet[] { return [attesterSlashing.attestation1, attesterSlashing.attestation2].map((attestation) => - getIndexedAttestationBigintSignatureSet(state, attestation) + getIndexedAttestationBytes8SignatureSet(state, attestation) ); } -export function getIndexedAttestationBigintSignatureSet( +export function getIndexedAttestationBytes8SignatureSet( state: CachedBeaconStateAllForks, - indexedAttestation: phase0.IndexedAttestationBigint + indexedAttestation: phase0.IndexedAttestationBytes8 ): ISignatureSet { const {index2pubkey} = state.epochCtx; - const slot = computeStartSlotAtEpoch(Number(indexedAttestation.data.target.epoch as bigint)); + const targetEpoch = indexedAttestation.data.target.epoch; + const slot = computeStartSlotAtEpoch(bytesToInt(targetEpoch)); const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot); return { type: SignatureSetType.aggregate, pubkeys: indexedAttestation.attestingIndices.map((i) => index2pubkey[i]), - signingRoot: computeSigningRoot(ssz.phase0.AttestationDataBigint, indexedAttestation.data, domain), + signingRoot: computeSigningRoot(ssz.phase0.AttestationDataBytes8, indexedAttestation.data, domain), signature: indexedAttestation.signature, }; } diff --git a/packages/state-transition/src/util/attestation.ts b/packages/state-transition/src/util/attestation.ts index cfa8f512e7d5..b67e977e1691 100644 --- a/packages/state-transition/src/util/attestation.ts +++ b/packages/state-transition/src/util/attestation.ts @@ -1,18 +1,23 @@ import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, Slot, ssz, ValidatorIndex} from "@lodestar/types"; +import {compareBytesLe} from "@lodestar/utils"; /** * Check if [[data1]] and [[data2]] are slashable according to Casper FFG rules. */ export function isSlashableAttestationData( - data1: phase0.AttestationDataBigint, - data2: phase0.AttestationDataBigint + data1: phase0.AttestationDataBytes8, + data2: phase0.AttestationDataBytes8 ): boolean { return ( // Double vote - (!ssz.phase0.AttestationDataBigint.equals(data1, data2) && data1.target.epoch === data2.target.epoch) || + (!ssz.phase0.AttestationDataBytes8.equals(data1, data2) && + // data1.target.epoch == data2.target.epoch + compareBytesLe(data1.target.epoch, data2.target.epoch) === 0) || // Surround vote - (data1.source.epoch < data2.source.epoch && data2.target.epoch < data1.target.epoch) + // (data1.source.epoch < data2.source.epoch && data2.target.epoch < data1.target.epoch) + (compareBytesLe(data1.source.epoch, data2.source.epoch) < 0 && + compareBytesLe(data2.target.epoch, data1.target.epoch) < 0) ); } diff --git a/packages/state-transition/test/perf/block/util.ts b/packages/state-transition/test/perf/block/util.ts index 5cbd78e1aa74..2e0879e7bdaa 100644 --- a/packages/state-transition/test/perf/block/util.ts +++ b/packages/state-transition/test/perf/block/util.ts @@ -71,22 +71,25 @@ export function getBlockPhase0( const startIndex = attesterSlashingStartIndex + i * bitsLen * exitedIndexStep; const attestingIndices = linspace(startIndex, bitsLen, exitedIndexStep); - const attData: phase0.AttestationDataBigint = { - slot: BigInt(attSlot), - index: BigInt(0), + const attData: phase0.AttestationData = { + slot: attSlot, + index: 0, beaconBlockRoot: rootA, - source: {epoch: BigInt(stateEpoch - 3), root: rootC}, - target: {epoch: BigInt(attEpoch), root: rootA}, + source: {epoch: stateEpoch - 3, root: rootC}, + target: {epoch: attEpoch, root: rootA}, }; + // deserialization similar to the real scenario where we receive data from gossipsub + const attDataBytes8 = ssz.phase0.AttestationDataBytes8.deserialize(ssz.phase0.AttestationData.serialize(attData)); + attesterSlashings.push({ attestation1: { attestingIndices, - data: attData, + data: attDataBytes8, signature: emptySig, }, attestation2: { attestingIndices, - data: {...attData, beaconBlockRoot: rootB}, + data: {...attDataBytes8, beaconBlockRoot: rootB}, signature: emptySig, }, }); diff --git a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts index 9e084dc783a3..503e5df3e83e 100644 --- a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts +++ b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts @@ -5,6 +5,7 @@ import {BitArray} from "@chainsafe/ssz"; import {config} from "@lodestar/config/default"; import {phase0, capella, ValidatorIndex, BLSSignature, ssz} from "@lodestar/types"; import {FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; +import {intToBytes} from "@lodestar/utils"; import {ZERO_HASH} from "../../../src/constants/index.js"; import {getBlockSignatureSets} from "../../../src/signatureSets/index.js"; import {generateCachedState} from "../../utils/state.js"; @@ -116,15 +117,15 @@ type IndexAttestationData = { function getMockAttesterSlashings(data1: IndexAttestationData, data2: IndexAttestationData): phase0.AttesterSlashing { return { - attestation1: getMockIndexAttestationBn(data1), - attestation2: getMockIndexAttestationBn(data2), + attestation1: getMockIndexAttestationBytes8(data1), + attestation2: getMockIndexAttestationBytes8(data2), }; } -function getMockIndexAttestationBn(data: IndexAttestationData): phase0.IndexedAttestationBigint { +function getMockIndexAttestationBytes8(data: IndexAttestationData): phase0.IndexedAttestationBytes8 { return { attestingIndices: data.attestingIndices, - data: getAttestationDataBigint(), + data: getAttestationDataBytes8(), signature: data.signature, }; } @@ -139,13 +140,13 @@ function getAttestationData(): phase0.AttestationData { }; } -function getAttestationDataBigint(): phase0.AttestationDataBigint { +function getAttestationDataBytes8(): phase0.AttestationDataBytes8 { return { - slot: BigInt(0), - index: BigInt(0), + slot: intToBytes(0, 8), + index: intToBytes(0, 8), beaconBlockRoot: ZERO_HASH, - source: {epoch: BigInt(0), root: ZERO_HASH}, - target: {epoch: BigInt(0), root: ZERO_HASH}, + source: {epoch: intToBytes(0, 8), root: ZERO_HASH}, + target: {epoch: intToBytes(0, 8), root: ZERO_HASH}, }; } diff --git a/packages/state-transition/test/unit/util/slashing.test.ts b/packages/state-transition/test/unit/util/slashing.test.ts index 411933080792..74bd660fc1f7 100644 --- a/packages/state-transition/test/unit/util/slashing.test.ts +++ b/packages/state-transition/test/unit/util/slashing.test.ts @@ -2,6 +2,7 @@ import {assert} from "chai"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {Epoch, phase0, ssz} from "@lodestar/types"; +import {intToBytes} from "@lodestar/utils"; import {isSlashableAttestationData} from "../../../src/util/index.js"; import {randBetween} from "../../utils/misc.js"; @@ -69,9 +70,9 @@ describe("isSlashableAttestationData", () => { }); }); -function getAttestationDataAt(sourceEpoch: Epoch, targetEpoch: Epoch): phase0.AttestationDataBigint { - const data = ssz.phase0.AttestationDataBigint.defaultValue(); - data.source.epoch = BigInt(sourceEpoch); - data.target.epoch = BigInt(targetEpoch); +function getAttestationDataAt(sourceEpoch: Epoch, targetEpoch: Epoch): phase0.AttestationDataBytes8 { + const data = ssz.phase0.AttestationDataBytes8.defaultValue(); + data.source.epoch = intToBytes(sourceEpoch, 8); + data.target.epoch = intToBytes(targetEpoch, 8); return data; } diff --git a/packages/types/package.json b/packages/types/package.json index e5e6d4fd5e25..833cf423e8be 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -70,6 +70,9 @@ "@chainsafe/ssz": "^0.14.0", "@lodestar/params": "^1.12.0" }, + "devDependencies": { + "@lodestar/utils": "^1.11.3" + }, "keywords": [ "ethereum", "eth-consensus", diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index ea06be588849..b50aaa830291 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -28,6 +28,7 @@ import { VALIDATOR_REGISTRY_LIMIT, } from "@lodestar/params"; import * as primitiveSsz from "../primitive/sszTypes.js"; +import {Bytes8} from "../primitive/sszTypes.js"; const { Boolean, @@ -104,9 +105,9 @@ export const Checkpoint = new ContainerType( ); /** Checkpoint where epoch is NOT bounded by the clock, so must be a bigint */ -export const CheckpointBigint = new ContainerType( +export const CheckpointBytes8 = new ContainerType( { - epoch: UintBn64, + epoch: Bytes8, root: Root, }, {typeName: "Checkpoint", jsonCase: "eth2"} @@ -264,14 +265,14 @@ export const AttestationData = new ContainerType( {typeName: "AttestationData", jsonCase: "eth2", cachePermanentRootStruct: true} ); -/** Same as `AttestationData` but epoch, slot and index are not bounded and must be a bigint */ -export const AttestationDataBigint = new ContainerType( +/** Same as `AttestationData` but epoch, slot and index are not bounded and must be a Uint8Array of 8 bytes */ +export const AttestationDataBytes8 = new ContainerType( { - slot: UintBn64, - index: UintBn64, + slot: Bytes8, + index: Bytes8, beaconBlockRoot: Root, - source: CheckpointBigint, - target: CheckpointBigint, + source: CheckpointBytes8, + target: CheckpointBytes8, }, {typeName: "AttestationData", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -285,11 +286,11 @@ export const IndexedAttestation = new ContainerType( {typeName: "IndexedAttestation", jsonCase: "eth2"} ); -/** Same as `IndexedAttestation` but epoch, slot and index are not bounded and must be a bigint */ -export const IndexedAttestationBigint = new ContainerType( +/** Same as `IndexedAttestation` but epoch, slot and index are not bounded and must be a Uint8Array of 8 bytes */ +export const IndexedAttestationBytes8 = new ContainerType( { attestingIndices: CommitteeIndices, - data: AttestationDataBigint, + data: AttestationDataBytes8, signature: BLSSignature, }, {typeName: "IndexedAttestation", jsonCase: "eth2"} @@ -330,8 +331,8 @@ export const AttesterSlashing = new ContainerType( // In state transition, AttesterSlashing attestations are only partially validated. Their slot and epoch could // be higher than the clock and the slashing would still be valid. Same applies to attestation data index, which // can be any arbitrary value. Must use bigint variants to hash correctly to all possible values - attestation1: IndexedAttestationBigint, - attestation2: IndexedAttestationBigint, + attestation1: IndexedAttestationBytes8, + attestation2: IndexedAttestationBytes8, }, {typeName: "AttesterSlashing", jsonCase: "eth2"} ); diff --git a/packages/types/src/phase0/types.ts b/packages/types/src/phase0/types.ts index 62d3d09ee92e..05b21a19b92c 100644 --- a/packages/types/src/phase0/types.ts +++ b/packages/types/src/phase0/types.ts @@ -20,9 +20,9 @@ export type HistoricalBatch = ValueOf; export type Validator = ValueOf; export type Validators = ValueOf; export type AttestationData = ValueOf; -export type AttestationDataBigint = ValueOf; +export type AttestationDataBytes8 = ValueOf; export type IndexedAttestation = ValueOf; -export type IndexedAttestationBigint = ValueOf; +export type IndexedAttestationBytes8 = ValueOf; export type PendingAttestation = ValueOf; export type SigningData = ValueOf; export type Attestation = ValueOf; diff --git a/packages/types/test/unit/phase0/sszTypes.test.ts b/packages/types/test/unit/phase0/sszTypes.test.ts new file mode 100644 index 000000000000..59de43418162 --- /dev/null +++ b/packages/types/test/unit/phase0/sszTypes.test.ts @@ -0,0 +1,70 @@ +import {expect} from "chai"; +import {intToBytes} from "@lodestar/utils"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {AttestationData, AttestationDataBytes8, Checkpoint, CheckpointBytes8} from "../../../src/phase0/sszTypes.js"; +import {phase0} from "../../../src/types.js"; + +describe("CheckpointBytes8", () => { + const epochs = [0, 1_000_000, Number.MAX_SAFE_INTEGER]; + + for (const epoch of epochs) { + it(`epoch ${epoch}`, () => { + const root = Buffer.alloc(32, 0x00); + const cp0 = { + epoch, + root, + }; + const cp1 = { + epoch: intToBytes(epoch, 8), + root, + }; + expect(Checkpoint.hashTreeRoot(cp0)).to.be.deep.equal(CheckpointBytes8.hashTreeRoot(cp1)); + expect(Checkpoint.serialize(cp0)).to.be.deep.equal(CheckpointBytes8.serialize(cp1)); + }); + } +}); + +describe("AttestationDataBytes8", () => { + const epochs = [0, 1_000_000, Number.MAX_SAFE_INTEGER]; + const root = Buffer.alloc(32, 0x00); + for (const epoch of epochs) { + it(`epoch ${epoch}`, () => { + const slot = SLOTS_PER_EPOCH * epoch; + const attestationData: phase0.AttestationData = { + slot, + index: 0, + beaconBlockRoot: root, + source: { + epoch, + root, + }, + target: { + epoch, + root, + }, + }; + + const attestationDataBytes8: phase0.AttestationDataBytes8 = { + slot: intToBytes(slot, 8), + index: intToBytes(0, 8), + beaconBlockRoot: root, + source: { + epoch: intToBytes(epoch, 8), + root, + }, + target: { + epoch: intToBytes(epoch, 8), + root, + }, + }; + + expect(AttestationData.hashTreeRoot(attestationData)).to.be.deep.equal( + AttestationDataBytes8.hashTreeRoot(attestationDataBytes8) + ); + + expect(AttestationData.serialize(attestationData)).to.be.deep.equal( + AttestationDataBytes8.serialize(attestationDataBytes8) + ); + }); + } +}); diff --git a/packages/types/test/unit/ssz.test.ts b/packages/types/test/unit/ssz.test.ts index 80ddcb12b893..6ade7d3cc248 100644 --- a/packages/types/test/unit/ssz.test.ts +++ b/packages/types/test/unit/ssz.test.ts @@ -14,12 +14,12 @@ describe("size", function () { describe("container serialization/deserialization field casing(s)", function () { it("AttesterSlashing", function () { const test = { - attestation1: ssz.phase0.IndexedAttestation.defaultValue(), - attestation2: ssz.phase0.IndexedAttestation.defaultValue(), + attestation1: ssz.phase0.IndexedAttestationBytes8.defaultValue(), + attestation2: ssz.phase0.IndexedAttestationBytes8.defaultValue(), }; const json = { - attestation_1: ssz.phase0.IndexedAttestation.toJson(test.attestation1), - attestation_2: ssz.phase0.IndexedAttestation.toJson(test.attestation2), + attestation_1: ssz.phase0.IndexedAttestationBytes8.toJson(test.attestation1), + attestation_2: ssz.phase0.IndexedAttestationBytes8.toJson(test.attestation2), }; const result = ssz.phase0.AttesterSlashing.fromJson(json); diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 5395c4315d22..92755c003812 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -46,6 +46,23 @@ export function bytesToBigInt(value: Uint8Array, endianness: Endianness = "le"): throw new Error("endianness must be either 'le' or 'be'"); } +/** + * Compare two byte arrays in LE. + * Instead of calling `a < b`, use `compareBytesLe(a, b) < 0`. + */ +export function compareBytesLe(a: Uint8Array, b: Uint8Array): number { + if (a.length !== b.length) { + throw new Error(`Lengths must be equal: ${a.length} !== ${b.length}`); + } + // Cannot use Buffer.compare() since this is LE + for (let i = a.length - 1; i >= 0; i--) { + if (a[i] !== b[i]) { + return a[i] < b[i] ? -1 : 1; + } + } + return 0; +} + export function toHex(buffer: Uint8Array | Parameters[0]): string { if (Buffer.isBuffer(buffer)) { return "0x" + buffer.toString("hex"); diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index b09625d7f135..f6dd609e16ee 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -1,6 +1,6 @@ import "../setup.js"; import {assert, expect} from "chai"; -import {intToBytes, bytesToInt} from "../../src/index.js"; +import {intToBytes, bytesToInt, compareBytesLe} from "../../src/index.js"; describe("intToBytes", () => { const zeroedArray = (length: number): number[] => Array.from({length}, () => 0); @@ -47,3 +47,21 @@ describe("bytesToInt", () => { }); } }); + +describe("compareBytesLe", () => { + const testCases: {a: number; b: number; expected: number}[] = [ + {a: 0, b: 0, expected: 0}, + {a: 0, b: 1, expected: -1}, + {a: 0, b: 1_000_000_000, expected: -1}, + {a: 10_000, b: 1_000_000_000, expected: -1}, + {a: 1, b: 0, expected: 1}, + {a: 1_000_000_000, b: 0, expected: 1}, + {a: 1_000_000_000, b: 10_000, expected: 1}, + ]; + + for (const {a, b, expected} of testCases) { + it(`should return ${expected} for ${a} and ${b}`, () => { + expect(compareBytesLe(intToBytes(a, 8), intToBytes(b, 8))).to.be.equal(expected); + }); + } +}); From 9485ac8c884b4dda46d113e9e0db067bdeaa3585 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 18 Nov 2023 16:24:50 +0700 Subject: [PATCH 2/9] fix: intToBytes and bytesToInt not to use BigInt --- packages/utils/src/bytes.ts | 40 +++++++++++++++++++++++++- packages/utils/test/unit/bytes.test.ts | 23 ++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 92755c003812..40d3cefe7ec8 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -18,6 +18,32 @@ export function toHexString(bytes: Uint8Array): string { * Return a byte array from a number or BigInt */ export function intToBytes(value: bigint | number, length: number, endianness: Endianness = "le"): Buffer { + if (typeof value === "number") { + // this is to avoid using BigInt + if (length === 2 || length === 4) { + const result = Buffer.alloc(length); + const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); + if (length === 2) { + dataView.setUint16(0, value, endianness === "le"); + } else { + dataView.setUint32(0, value, endianness === "le"); + } + return result; + } else if (length === 8) { + const result = Buffer.alloc(8); + const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); + const leastSignificant = (value & 0xffffffff) >>> 0; + const mostSignificant = value > 0xffffffff ? Math.floor((value - leastSignificant) / 0xffffffff) : 0; + if (endianness === "le") { + dataView.setUint32(0, leastSignificant, true); + dataView.setUint32(4, mostSignificant, true); + } else { + dataView.setUint32(0, mostSignificant, false); + dataView.setUint32(4, leastSignificant, false); + } + return result; + } + } return bigIntToBytes(BigInt(value), length, endianness); } @@ -25,7 +51,19 @@ export function intToBytes(value: bigint | number, length: number, endianness: E * Convert byte array in LE to integer. */ export function bytesToInt(value: Uint8Array, endianness: Endianness = "le"): number { - return Number(bytesToBigInt(value, endianness)); + let result = 0; + if (endianness === "le") { + for (let i = 0; i < value.length; i++) { + // result += (value[i] << (8 * i)) >>> 0; + result += value[i] * Math.pow(2, 8 * i); + } + } else { + for (let i = 0; i < value.length; i++) { + result += (value[i] << (8 * (value.length - 1 - i))) >>> 0; + } + } + + return result; } export function bigIntToBytes(value: bigint, length: number, endianness: Endianness = "le"): Buffer { diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index f6dd609e16ee..0027e3ea5623 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -24,12 +24,24 @@ describe("intToBytes", () => { {input: [BigInt(65535), 96], output: Buffer.from([255, 255, ...zeroedArray(96 - 2)])}, ]; for (const {input, output} of testCases) { - const type = typeof input; + const type = typeof input[0]; const length = input[1]; it(`should correctly serialize ${type} to bytes length ${length}`, () => { assert(intToBytes(input[0], input[1]).equals(output)); }); } + + it("random check 10_000 numbers", () => { + for (let i = 0; i < 10_000; i++) { + const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + for (const length of [2, 4, 8]) { + assert( + intToBytes(value, length).equals(intToBytes(BigInt(value), length)), + `failed at value ${value} and length ${length}` + ); + } + } + }); }); describe("bytesToInt", () => { @@ -46,6 +58,15 @@ describe("bytesToInt", () => { expect(bytesToInt(input)).to.be.equal(output); }); } + + it("random check 10_000 numbers", () => { + for (let i = 0; i < 10_000; i++) { + const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + const length = 8; + const bytes = intToBytes(value, length); + expect(bytesToInt(bytes)).to.be.equal(value, `failed at value ${value} and length ${length}`); + } + }); }); describe("compareBytesLe", () => { From 8428fe05aceba582f35697492f530767e1e95bb5 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Sat, 18 Nov 2023 18:48:46 +0700 Subject: [PATCH 3/9] chore: new Bytes8UintJson type --- packages/types/package.json | 4 +--- packages/types/src/phase0/sszTypes.ts | 2 +- packages/types/src/primitive/sszTypes.ts | 23 ++++++++++++++++++ packages/types/test/unit/ssz.test.ts | 30 ++++++++++++++---------- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index 833cf423e8be..34a50a528d7f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -68,11 +68,9 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/ssz": "^0.14.0", + "@lodestar/utils": "^1.12.0", "@lodestar/params": "^1.12.0" }, - "devDependencies": { - "@lodestar/utils": "^1.11.3" - }, "keywords": [ "ethereum", "eth-consensus", diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index b50aaa830291..5527fad14390 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -28,7 +28,7 @@ import { VALIDATOR_REGISTRY_LIMIT, } from "@lodestar/params"; import * as primitiveSsz from "../primitive/sszTypes.js"; -import {Bytes8} from "../primitive/sszTypes.js"; +import {Bytes8UintJson as Bytes8} from "../primitive/sszTypes.js"; const { Boolean, diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 65c81d1247b9..9c4572e4dba2 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -1,4 +1,5 @@ import {ByteVectorType, UintNumberType, UintBigintType, BooleanType} from "@chainsafe/ssz"; +import {bytesToBigInt, intToBytes} from "@lodestar/utils"; export const Boolean = new BooleanType(); export const Byte = new UintNumberType(1); @@ -62,3 +63,25 @@ export const BLSSignature = Bytes96; export const Domain = Bytes32; export const ParticipationFlags = new UintNumberType(1, {setBitwiseOR: true}); export const ExecutionAddress = Bytes20; + +/** + * Custom implementation of ByteVectorType supporting uint type json. + * This is mainly for the spec test where numbers are used in AttesterSlashing but we model them as Uint8Array instead. + */ +export class ByteVectorTypeUintJson extends ByteVectorType { + fromJson(json: unknown): Uint8Array { + if (typeof json === "number" || typeof json === "bigint") { + return intToBytes(json, this.fixedSize); + } else if (typeof json === "string") { + const num = parseInt(json, 10); + return intToBytes(num, this.fixedSize); + } + throw new Error(`Invalid json type ${typeof json}`); + } + + toJson(value: Uint8Array): unknown { + return bytesToBigInt(value).toString(); + } +} + +export const Bytes8UintJson = new ByteVectorTypeUintJson(8); diff --git a/packages/types/test/unit/ssz.test.ts b/packages/types/test/unit/ssz.test.ts index 6ade7d3cc248..76645bbf4fae 100644 --- a/packages/types/test/unit/ssz.test.ts +++ b/packages/types/test/unit/ssz.test.ts @@ -1,5 +1,6 @@ import {expect} from "chai"; import {ssz} from "../../src/index.js"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; describe("size", function () { it("should calculate correct minSize and maxSize", () => { @@ -13,18 +14,23 @@ describe("size", function () { describe("container serialization/deserialization field casing(s)", function () { it("AttesterSlashing", function () { - const test = { - attestation1: ssz.phase0.IndexedAttestationBytes8.defaultValue(), - attestation2: ssz.phase0.IndexedAttestationBytes8.defaultValue(), - }; - const json = { - attestation_1: ssz.phase0.IndexedAttestationBytes8.toJson(test.attestation1), - attestation_2: ssz.phase0.IndexedAttestationBytes8.toJson(test.attestation2), - }; - - const result = ssz.phase0.AttesterSlashing.fromJson(json); - const back = ssz.phase0.AttesterSlashing.toJson(result); - expect(back).to.be.deep.equal(json); + const epochs = [0, 10, 1_000_000]; + for (const epoch of epochs) { + const test = { + attestation1: ssz.phase0.IndexedAttestation.defaultValue(), + attestation2: ssz.phase0.IndexedAttestation.defaultValue(), + }; + test.attestation1.data.slot = epoch * SLOTS_PER_EPOCH; + test.attestation1.data.source.epoch = epoch; + test.attestation1.data.target.epoch = epoch + 1; + const json = { + attestation_1: ssz.phase0.IndexedAttestation.toJson(test.attestation1), + attestation_2: ssz.phase0.IndexedAttestation.toJson(test.attestation2), + }; + const result = ssz.phase0.AttesterSlashing.fromJson(json); + const back = ssz.phase0.AttesterSlashing.toJson(result); + expect(back).to.be.deep.equal(json); + } }); it("ProposerSlashing", function () { From 3e14192757d098688c1233628e493018b73381c9 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 20 Nov 2023 15:12:15 +0700 Subject: [PATCH 4/9] fix: handle big number passed to config.getForkName() --- packages/beacon-node/src/network/network.ts | 6 +++-- packages/utils/src/bytes.ts | 27 ++++++++++----------- packages/utils/test/unit/bytes.test.ts | 16 ++++++++---- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 1463fef9180b..c9aa2aaab970 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -351,8 +351,10 @@ export class Network implements INetwork { } async publishAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): Promise { - const slot = attesterSlashing.attestation1.data.slot; - const fork = this.config.getForkName(bytesToInt(slot)); + const slotBuffer = attesterSlashing.attestation1.data.slot; + // Buffer representation of Number.MAX_SAFE_INTEGER is + const slot = slotBuffer[6] >= 0x1f ? Number.MAX_SAFE_INTEGER : bytesToInt(slotBuffer); + const fork = this.config.getForkName(slot); return this.publishGossip( {type: GossipType.attester_slashing, fork}, attesterSlashing diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 40d3cefe7ec8..27f6ec156464 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -18,20 +18,16 @@ export function toHexString(bytes: Uint8Array): string { * Return a byte array from a number or BigInt */ export function intToBytes(value: bigint | number, length: number, endianness: Endianness = "le"): Buffer { - if (typeof value === "number") { + if (typeof value === "number" && (length === 2 || length === 4 || length === 8)) { // this is to avoid using BigInt - if (length === 2 || length === 4) { - const result = Buffer.alloc(length); - const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); - if (length === 2) { - dataView.setUint16(0, value, endianness === "le"); - } else { - dataView.setUint32(0, value, endianness === "le"); - } - return result; - } else if (length === 8) { - const result = Buffer.alloc(8); - const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); + const result = Buffer.alloc(length); + const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); + if (length === 2) { + dataView.setUint16(0, value, endianness === "le"); + } else if (length === 4) { + dataView.setUint32(0, value, endianness === "le"); + } else { + // length === 8 const leastSignificant = (value & 0xffffffff) >>> 0; const mostSignificant = value > 0xffffffff ? Math.floor((value - leastSignificant) / 0xffffffff) : 0; if (endianness === "le") { @@ -41,9 +37,12 @@ export function intToBytes(value: bigint | number, length: number, endianness: E dataView.setUint32(0, mostSignificant, false); dataView.setUint32(4, leastSignificant, false); } - return result; } + + return result; } + + // only fallback to bigIntToBytes to avoid having to use BigInt return bigIntToBytes(BigInt(value), length, endianness); } diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 0027e3ea5623..bb62b51e0c6d 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -31,13 +31,18 @@ describe("intToBytes", () => { }); } - it("random check 10_000 numbers", () => { - for (let i = 0; i < 10_000; i++) { + const numTestCases = 10_000; + it(`random check ${numTestCases} numbers`, () => { + for (let i = 0; i < numTestCases; i++) { const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); for (const length of [2, 4, 8]) { assert( intToBytes(value, length).equals(intToBytes(BigInt(value), length)), - `failed at value ${value} and length ${length}` + `failed at value ${value} and length ${length} le` + ); + assert( + intToBytes(value, length, "be").equals(intToBytes(BigInt(value), length, "be")), + `failed at value ${value} and length ${length} be` ); } } @@ -59,8 +64,9 @@ describe("bytesToInt", () => { }); } - it("random check 10_000 numbers", () => { - for (let i = 0; i < 10_000; i++) { + const numTetstCases = 100_000; + it(`random check ${numTetstCases} numbers`, () => { + for (let i = 0; i < numTetstCases; i++) { const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); const length = 8; const bytes = intToBytes(value, length); From 6d7d40174a6e67c37e689ee509e5ab79ed2a2d6b Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 20 Nov 2023 16:46:19 +0700 Subject: [PATCH 5/9] fix: improve performance using Buffer apis --- packages/utils/package.json | 2 +- packages/utils/src/bytes.ts | 48 ++++++++++++++++++-- packages/utils/test/perf/bytes.test.ts | 61 ++++++++++++++++++++++++++ packages/utils/test/unit/bytes.test.ts | 20 +++++++-- 4 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 packages/utils/test/perf/bytes.test.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index 457218fa4f4c..afa176410c4f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -31,7 +31,7 @@ "lint": "eslint --color --ext .ts src/ test/", "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", - "test:unit": "mocha 'test/**/*.test.ts'", + "test:unit": "mocha 'test/unit/**/*.test.ts'", "test:browsers": "yarn karma start karma.config.cjs", "check-readme": "typescript-docs-verifier" }, diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 27f6ec156464..81090148df98 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -18,8 +18,33 @@ export function toHexString(bytes: Uint8Array): string { * Return a byte array from a number or BigInt */ export function intToBytes(value: bigint | number, length: number, endianness: Endianness = "le"): Buffer { + // Buffer api only support up to 6 bytes + // otherwise got "RangeError [ERR_OUT_OF_RANGE]: The value of "value" is out of range. It must be >= 0 and < 2 ** 48" + if (typeof value === "number" && value < Math.pow(2, 48)) { + const buffer = Buffer.alloc(length); + if (endianness === "le") { + // writeUintLE only supports 1 to 6 byteLength + buffer.writeUintLE(value, 0, Math.min(length, 6)); + } else { + // writeUintBE only supports 1 to 6 byteLength + const bytesLength = Math.min(length, 6); + const offset = Math.max(0, length - bytesLength); + buffer.writeUintBE(value, offset, Math.min(length, 6)); + } + return buffer; + } + + return intToBytesVanilla(value, length, endianness); +} + +/** + * Same function to intToBytes but we compute ourself if possible to avoid using BigInt. + * See https://github.com/ChainSafe/lodestar/issues/5892 + * Do not use this function directly, it's separated for testing only + */ +export function intToBytesVanilla(value: bigint | number, length: number, endianness: Endianness = "le"): Buffer { if (typeof value === "number" && (length === 2 || length === 4 || length === 8)) { - // this is to avoid using BigInt + // compute ourself if possible const result = Buffer.alloc(length); const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); if (length === 2) { @@ -38,11 +63,10 @@ export function intToBytes(value: bigint | number, length: number, endianness: E dataView.setUint32(4, leastSignificant, false); } } - return result; } - // only fallback to bigIntToBytes to avoid having to use BigInt + // only fallback to bigIntToBytes to avoid having to use BigInt if possible return bigIntToBytes(BigInt(value), length, endianness); } @@ -50,10 +74,26 @@ export function intToBytes(value: bigint | number, length: number, endianness: E * Convert byte array in LE to integer. */ export function bytesToInt(value: Uint8Array, endianness: Endianness = "le"): number { + // use Buffer api if possible since it's the fastest + // it only supports up to 6 bytes through + if (endianness === "le") { + const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength); + if (value.length <= 8 && value[6] === 0 && value[7] === 0) { + return buffer.readUintLE(0, Math.min(value.length, 6)); + } + } else { + const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength); + if (value.length <= 8 && value[0] === 0 && value[1] === 0) { + const bytesLength = Math.min(value.length, 6); + const offset = Math.max(0, length - bytesLength); + return buffer.readUintBE(offset, bytesLength); + } + } + + // otherwise compute manually let result = 0; if (endianness === "le") { for (let i = 0; i < value.length; i++) { - // result += (value[i] << (8 * i)) >>> 0; result += value[i] * Math.pow(2, 8 * i); } } else { diff --git a/packages/utils/test/perf/bytes.test.ts b/packages/utils/test/perf/bytes.test.ts new file mode 100644 index 000000000000..23c807af99f0 --- /dev/null +++ b/packages/utils/test/perf/bytes.test.ts @@ -0,0 +1,61 @@ +import {itBench, setBenchOpts} from "@dapplion/benchmark"; +import {bigIntToBytes, bytesToBigInt, bytesToInt, intToBytes} from "../../src/bytes.js"; + +describe("bytesToInt", () => { + const runsFactor = 1000; + // bound to Math.pow(2, 48) because that matches Buffer api max value and that matches our use case + const maxValue = Math.pow(2, 48); + setBenchOpts({ + minMs: 10_000, + }); + + itBench({ + id: "intToBytes", + beforeEach: () => Math.floor(Math.random() * maxValue), + fn: (value) => { + for (let i = 0; i < runsFactor; i++) { + intToBytes(value, 8); + } + }, + runsFactor, + }); + + // old implementation of intToBytes + itBench({ + id: "bigIntToBytes", + beforeEach: () => Math.floor(Math.random() * maxValue), + fn: (value) => { + for (let i = 0; i < runsFactor; i++) { + bigIntToBytes(BigInt(value), 8); + } + }, + runsFactor, + }); + + itBench({ + id: "bytesToInt", + beforeEach: () => Math.floor(Math.random() * maxValue), + fn: (value) => { + const length = 8; + const bytes = intToBytes(value, length); + for (let i = 0; i < runsFactor; i++) { + bytesToInt(bytes); + } + }, + runsFactor, + }); + + itBench({ + // old implementation of bytesToInt + id: "bytesToBigInt", + beforeEach: () => Math.floor(Math.random() * maxValue), + fn: (value) => { + const length = 8; + const bytes = intToBytes(value, length); + for (let i = 0; i < runsFactor; i++) { + bytesToBigInt(bytes); + } + }, + runsFactor, + }); +}); diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index bb62b51e0c6d..700ee9d86440 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -1,6 +1,6 @@ import "../setup.js"; import {assert, expect} from "chai"; -import {intToBytes, bytesToInt, compareBytesLe} from "../../src/index.js"; +import {intToBytes, bytesToInt, compareBytesLe, intToBytesVanilla} from "../../src/index.js"; describe("intToBytes", () => { const zeroedArray = (length: number): number[] => Array.from({length}, () => 0); @@ -34,8 +34,12 @@ describe("intToBytes", () => { const numTestCases = 10_000; it(`random check ${numTestCases} numbers`, () => { for (let i = 0; i < numTestCases; i++) { - const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); - for (const length of [2, 4, 8]) { + for (const [length, maxValue] of [ + [2, 0xffff], + [4, 0xffffffff], + [8, Number.MAX_SAFE_INTEGER], + ]) { + const value = Math.floor(Math.random() * maxValue); assert( intToBytes(value, length).equals(intToBytes(BigInt(value), length)), `failed at value ${value} and length ${length} le` @@ -44,6 +48,14 @@ describe("intToBytes", () => { intToBytes(value, length, "be").equals(intToBytes(BigInt(value), length, "be")), `failed at value ${value} and length ${length} be` ); + assert( + intToBytesVanilla(value, length).equals(intToBytes(BigInt(value), length)), + `failed at value ${value} and length ${length} le` + ); + assert( + intToBytesVanilla(value, length, "be").equals(intToBytes(BigInt(value), length, "be")), + `failed at value ${value} and length ${length} be` + ); } } }); @@ -64,7 +76,7 @@ describe("bytesToInt", () => { }); } - const numTetstCases = 100_000; + const numTetstCases = 10_000; it(`random check ${numTetstCases} numbers`, () => { for (let i = 0; i < numTetstCases; i++) { const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); From 3cbe133b45dd43d36c983f1b3bdda241e50df62c Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 21 Nov 2023 10:01:08 +0700 Subject: [PATCH 6/9] fix: typo in intToBytes --- packages/utils/src/bytes.ts | 10 +++++----- packages/utils/test/unit/bytes.test.ts | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 81090148df98..20d826568c06 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -77,15 +77,15 @@ export function bytesToInt(value: Uint8Array, endianness: Endianness = "le"): nu // use Buffer api if possible since it's the fastest // it only supports up to 6 bytes through if (endianness === "le") { - const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength); if (value.length <= 8 && value[6] === 0 && value[7] === 0) { + const buffer = Buffer.from(value); return buffer.readUintLE(0, Math.min(value.length, 6)); } } else { - const buffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength); if (value.length <= 8 && value[0] === 0 && value[1] === 0) { + const buffer = Buffer.from(value); const bytesLength = Math.min(value.length, 6); - const offset = Math.max(0, length - bytesLength); + const offset = Math.max(0, value.length - bytesLength); return buffer.readUintBE(offset, bytesLength); } } @@ -97,8 +97,8 @@ export function bytesToInt(value: Uint8Array, endianness: Endianness = "le"): nu result += value[i] * Math.pow(2, 8 * i); } } else { - for (let i = 0; i < value.length; i++) { - result += (value[i] << (8 * (value.length - 1 - i))) >>> 0; + for (let i = value.length - 1; i >= 0; i--) { + result += value[i] * Math.pow(2, 8 * (value.length - 1 - i)); } } diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 700ee9d86440..3f4c7a523335 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -81,8 +81,10 @@ describe("bytesToInt", () => { for (let i = 0; i < numTetstCases; i++) { const value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); const length = 8; - const bytes = intToBytes(value, length); - expect(bytesToInt(bytes)).to.be.equal(value, `failed at value ${value} and length ${length}`); + const bytesLE = intToBytes(value, length); + expect(bytesToInt(bytesLE)).to.be.equal(value, `le - failed at value ${value} and length ${length}`); + const bytesBE = intToBytes(value, length, "be"); + expect(bytesToInt(bytesBE, "be")).to.be.equal(value, `be - failed at value ${value} and length ${length}`); } }); }); From 94b18f545f6fb081ddf294c933bf3d27e72b8452 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 21 Nov 2023 11:09:45 +0700 Subject: [PATCH 7/9] feat: also use Bytes8 for ProposerSlashing --- packages/beacon-node/src/network/network.ts | 9 ++--- .../chain/validation/proposerSlashing.test.ts | 9 ++--- packages/config/package.json | 1 + packages/config/src/forkConfig/index.ts | 7 +++- packages/config/src/forkConfig/types.ts | 4 ++- packages/flare/src/cmds/selfSlashAttester.ts | 8 ++--- packages/flare/src/cmds/selfSlashProposer.ts | 22 ++++++------ .../src/block/processProposerSlashing.ts | 9 +++-- .../src/signatureSets/attesterSlashings.ts | 8 ++--- .../src/signatureSets/proposerSlashings.ts | 9 ++--- .../state-transition/src/util/attestation.ts | 2 +- .../state-transition/test/perf/block/util.ts | 5 +-- .../unit/signatureSets/signatureSets.test.ts | 8 ++--- packages/types/src/phase0/sszTypes.ts | 22 ++++++------ packages/types/src/phase0/types.ts | 4 +-- packages/utils/src/bytes.ts | 22 +++++++++++- packages/utils/test/unit/bytes.test.ts | 34 ++++++++++++++++++- 17 files changed, 120 insertions(+), 63 deletions(-) diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index c9aa2aaab970..eba7351facdd 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -2,7 +2,7 @@ import {PeerId} from "@libp2p/interface/peer-id"; import {PublishOpts} from "@chainsafe/libp2p-gossipsub/types"; import {PeerScoreStatsDump} from "@chainsafe/libp2p-gossipsub/score"; import {BeaconConfig} from "@lodestar/config"; -import {bytesToInt, sleep} from "@lodestar/utils"; +import {sleep} from "@lodestar/utils"; import {LoggerNode} from "@lodestar/logger/node"; import {computeStartSlotAtEpoch, computeTimeAtSlot} from "@lodestar/state-transition"; import {phase0, allForks, deneb, altair, Root, capella, SlotRootHex} from "@lodestar/types"; @@ -343,7 +343,7 @@ export class Network implements INetwork { } async publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise { - const fork = this.config.getForkName(Number(proposerSlashing.signedHeader1.message.slot as bigint)); + const fork = this.config.getForkNameBytes8(proposerSlashing.signedHeader1.message.slot); return this.publishGossip( {type: GossipType.proposer_slashing, fork}, proposerSlashing @@ -351,10 +351,7 @@ export class Network implements INetwork { } async publishAttesterSlashing(attesterSlashing: phase0.AttesterSlashing): Promise { - const slotBuffer = attesterSlashing.attestation1.data.slot; - // Buffer representation of Number.MAX_SAFE_INTEGER is - const slot = slotBuffer[6] >= 0x1f ? Number.MAX_SAFE_INTEGER : bytesToInt(slotBuffer); - const fork = this.config.getForkName(slot); + const fork = this.config.getForkNameBytes8(attesterSlashing.attestation1.data.slot); return this.publishGossip( {type: GossipType.attester_slashing, fork}, attesterSlashing diff --git a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts index de172c0ec136..4c8ac6bd8e97 100644 --- a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts @@ -1,5 +1,6 @@ import {describe, it, beforeEach, afterEach, vi} from "vitest"; import {phase0, ssz} from "@lodestar/types"; +import {intToBytes} from "@lodestar/utils"; import {generateCachedState} from "../../../utils/state.js"; import {ProposerSlashingErrorCode} from "../../../../src/chain/errors/proposerSlashingError.js"; import {validateGossipProposerSlashing} from "../../../../src/chain/validation/proposerSlashing.js"; @@ -36,8 +37,8 @@ describe("validate proposer slashing", () => { it("should return invalid proposer slashing - invalid", async () => { const proposerSlashing = ssz.phase0.ProposerSlashing.defaultValue(); // Make it invalid - proposerSlashing.signedHeader1.message.slot = BigInt(1); - proposerSlashing.signedHeader2.message.slot = BigInt(0); + proposerSlashing.signedHeader1.message.slot = intToBytes(1, 8); + proposerSlashing.signedHeader2.message.slot = intToBytes(0, 8); await expectRejectedWithLodestarError( validateGossipProposerSlashing(chainStub, proposerSlashing), @@ -46,8 +47,8 @@ describe("validate proposer slashing", () => { }); it("should return valid proposer slashing", async () => { - const signedHeader1 = ssz.phase0.SignedBeaconBlockHeaderBigint.defaultValue(); - const signedHeader2 = ssz.phase0.SignedBeaconBlockHeaderBigint.defaultValue(); + const signedHeader1 = ssz.phase0.SignedBeaconBlockHeaderBytes8.defaultValue(); + const signedHeader2 = ssz.phase0.SignedBeaconBlockHeaderBytes8.defaultValue(); // Make it different, so slashable signedHeader2.message.stateRoot = Buffer.alloc(32, 1); diff --git a/packages/config/package.json b/packages/config/package.json index 7814c9a18778..467694ee9db4 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -66,6 +66,7 @@ "dependencies": { "@chainsafe/ssz": "^0.14.0", "@lodestar/params": "^1.12.0", + "@lodestar/utils": "^1.12.0", "@lodestar/types": "^1.12.0" } } diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index d630f1ddfc88..49f28830a2ab 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -7,7 +7,8 @@ import { isForkExecution, isForkBlobs, } from "@lodestar/params"; -import {Slot, allForks, Version, ssz} from "@lodestar/types"; +import {Slot, allForks, Version, ssz, Bytes8} from "@lodestar/types"; +import {bytesToIntOrMaxInt} from "@lodestar/utils"; import {ChainConfig} from "../chainConfig/index.js"; import {ForkConfig, ForkInfo} from "./types.js"; @@ -81,6 +82,10 @@ export function createForkConfig(config: ChainConfig): ForkConfig { getForkName(slot: Slot): ForkName { return this.getForkInfo(slot).name; }, + getForkNameBytes8(slotBufferLE: Bytes8): ForkName { + const slot = bytesToIntOrMaxInt(slotBufferLE); + return this.getForkName(slot); + }, getForkSeq(slot: Slot): ForkSeq { return this.getForkInfo(slot).seq; }, diff --git a/packages/config/src/forkConfig/types.ts b/packages/config/src/forkConfig/types.ts index b61752bf3698..676aa21e0185 100644 --- a/packages/config/src/forkConfig/types.ts +++ b/packages/config/src/forkConfig/types.ts @@ -1,5 +1,5 @@ import {ForkName, ForkSeq} from "@lodestar/params"; -import {allForks, Epoch, Slot, Version} from "@lodestar/types"; +import {allForks, Bytes8, Epoch, Slot, Version} from "@lodestar/types"; export type ForkInfo = { name: ForkName; @@ -24,6 +24,8 @@ export type ForkConfig = { /** Get the hard-fork name at a given slot */ getForkName(slot: Slot): ForkName; + /** Get the hard-fork name at a given Uint8ArrayLE slot */ + getForkNameBytes8(slot: Bytes8): ForkName; /** Get the hard-fork sequence number at a given slot */ getForkSeq(slot: Slot): ForkSeq; /** Get the hard-fork version at a given slot */ diff --git a/packages/flare/src/cmds/selfSlashAttester.ts b/packages/flare/src/cmds/selfSlashAttester.ts index 89fb1f71d8b6..7589b75f367e 100644 --- a/packages/flare/src/cmds/selfSlashAttester.ts +++ b/packages/flare/src/cmds/selfSlashAttester.ts @@ -5,7 +5,7 @@ import {phase0, ssz} from "@lodestar/types"; import {config as chainConfig} from "@lodestar/config/default"; import {createBeaconConfig, BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_ATTESTER, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; -import {bytesToInt, intToBytes, toHexString} from "@lodestar/utils"; +import {intToBytes, toHexString} from "@lodestar/utils"; import {computeSigningRoot} from "@lodestar/state-transition"; import {CliCommand} from "../util/command.js"; import {deriveSecretKeys, SecretKeysArgs, secretKeysOptions} from "../util/deriveSecretKeys.js"; @@ -51,7 +51,7 @@ export const selfSlashAttester: CliCommand, export async function selfSlashAttesterHandler(args: SelfSlashArgs): Promise { const sksAll = deriveSecretKeys(args); - const slot = Number(args.slot); // Throws if not valid + const slot = BigInt(args.slot); // Throws if not valid const batchSize = parseInt(args.batchSize); if (isNaN(batchSize)) throw Error(`Invalid arg batchSize ${args.batchSize}`); @@ -148,8 +148,8 @@ function signAttestationDataBytes8( sks: SecretKey[], data: phase0.AttestationDataBytes8 ): Uint8Array { - const slot = bytesToInt(data.slot); - const proposerDomain = config.getDomain(slot, DOMAIN_BEACON_ATTESTER); + const fork = config.getForkNameBytes8(data.slot); + const proposerDomain = config.getDomainAtFork(fork, DOMAIN_BEACON_ATTESTER); const signingRoot = computeSigningRoot(ssz.phase0.AttestationDataBytes8, data, proposerDomain); const sigs = sks.map((sk) => sk.sign(signingRoot)); diff --git a/packages/flare/src/cmds/selfSlashProposer.ts b/packages/flare/src/cmds/selfSlashProposer.ts index 49675bb802de..95a8e13a19fc 100644 --- a/packages/flare/src/cmds/selfSlashProposer.ts +++ b/packages/flare/src/cmds/selfSlashProposer.ts @@ -4,7 +4,7 @@ import {phase0, ssz} from "@lodestar/types"; import {config as chainConfig} from "@lodestar/config/default"; import {createBeaconConfig, BeaconConfig} from "@lodestar/config"; import {DOMAIN_BEACON_PROPOSER} from "@lodestar/params"; -import {toHexString} from "@lodestar/utils"; +import {intToBytes, toHexString} from "@lodestar/utils"; import {computeSigningRoot} from "@lodestar/state-transition"; import {CliCommand} from "../util/command.js"; import {deriveSecretKeys, SecretKeysArgs, secretKeysOptions} from "../util/deriveSecretKeys.js"; @@ -99,15 +99,15 @@ export async function selfSlashProposerHandler(args: SelfSlashArgs): Promise { - const domain = state.config.getDomain( - state.slot, - DOMAIN_BEACON_PROPOSER, - Number(signedHeader.message.slot as bigint) - ); + const fork = state.config.getForkNameBytes8(signedHeader.message.slot); + const domain = state.config.getDomainAtFork(fork, DOMAIN_BEACON_PROPOSER); return { type: SignatureSetType.single, pubkey, - signingRoot: computeSigningRoot(ssz.phase0.BeaconBlockHeaderBigint, signedHeader.message, domain), + signingRoot: computeSigningRoot(ssz.phase0.BeaconBlockHeaderBytes8, signedHeader.message, domain), signature: signedHeader.signature, }; }); diff --git a/packages/state-transition/src/util/attestation.ts b/packages/state-transition/src/util/attestation.ts index b67e977e1691..b5ca9a367c46 100644 --- a/packages/state-transition/src/util/attestation.ts +++ b/packages/state-transition/src/util/attestation.ts @@ -12,7 +12,7 @@ export function isSlashableAttestationData( return ( // Double vote (!ssz.phase0.AttestationDataBytes8.equals(data1, data2) && - // data1.target.epoch == data2.target.epoch + // data1.target.epoch == data2.target.epoch compareBytesLe(data1.target.epoch, data2.target.epoch) === 0) || // Surround vote // (data1.source.epoch < data2.source.epoch && data2.target.epoch < data1.target.epoch) diff --git a/packages/state-transition/test/perf/block/util.ts b/packages/state-transition/test/perf/block/util.ts index 2e0879e7bdaa..acc1838cfd1e 100644 --- a/packages/state-transition/test/perf/block/util.ts +++ b/packages/state-transition/test/perf/block/util.ts @@ -4,6 +4,7 @@ import {BitArray} from "@chainsafe/ssz"; import {altair, phase0, ssz} from "@lodestar/types"; import {DOMAIN_DEPOSIT, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import {config} from "@lodestar/config/default"; +import {intToBytes} from "@lodestar/utils/src/bytes.js"; import { computeDomain, computeEpochAtSlot, @@ -53,11 +54,11 @@ export function getBlockPhase0( const proposerIndex = proposerSlashingStartIndex + i * exitedIndexStep; proposerSlashings.push({ signedHeader1: { - message: {slot: BigInt(1_800_000), proposerIndex, parentRoot: rootA, stateRoot: rootB, bodyRoot: rootC}, + message: {slot: intToBytes(1_800_000, 8), proposerIndex, parentRoot: rootA, stateRoot: rootB, bodyRoot: rootC}, signature: emptySig, }, signedHeader2: { - message: {slot: BigInt(1_800_000), proposerIndex, parentRoot: rootC, stateRoot: rootA, bodyRoot: rootB}, + message: {slot: intToBytes(1_800_000, 8), proposerIndex, parentRoot: rootC, stateRoot: rootA, bodyRoot: rootB}, signature: emptySig, }, }); diff --git a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts index 503e5df3e83e..78dc587104c5 100644 --- a/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts +++ b/packages/state-transition/test/unit/signatureSets/signatureSets.test.ts @@ -92,15 +92,15 @@ type BlockProposerData = { function getMockProposerSlashings(data1: BlockProposerData, data2: BlockProposerData): phase0.ProposerSlashing { return { - signedHeader1: getMockSignedBeaconBlockHeaderBigint(data1), - signedHeader2: getMockSignedBeaconBlockHeaderBigint(data2), + signedHeader1: getMockSignedBeaconBlockHeaderBytes8(data1), + signedHeader2: getMockSignedBeaconBlockHeaderBytes8(data2), }; } -function getMockSignedBeaconBlockHeaderBigint(data: BlockProposerData): phase0.SignedBeaconBlockHeaderBigint { +function getMockSignedBeaconBlockHeaderBytes8(data: BlockProposerData): phase0.SignedBeaconBlockHeaderBytes8 { return { message: { - slot: BigInt(0), + slot: intToBytes(0, 8), proposerIndex: data.proposerIndex, parentRoot: ZERO_HASH, stateRoot: ZERO_HASH, diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 5527fad14390..73f0cd7b5e97 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -66,10 +66,10 @@ export const BeaconBlockHeader = new ContainerType( {typeName: "BeaconBlockHeader", jsonCase: "eth2", cachePermanentRootStruct: true} ); -/** BeaconBlockHeader where slot is NOT bounded by the clock, i.e. slashings. So slot is a bigint. */ -export const BeaconBlockHeaderBigint = new ContainerType( +/** BeaconBlockHeader where slot is NOT bounded by the clock, i.e. slashings. So slot is a Uint8Array LE (instead of BigInt to improve performance). */ +export const BeaconBlockHeaderBytes8 = new ContainerType( { - slot: UintBn64, + slot: Bytes8, proposerIndex: ValidatorIndex, parentRoot: Root, stateRoot: Root, @@ -86,10 +86,10 @@ export const SignedBeaconBlockHeader = new ContainerType( {typeName: "SignedBeaconBlockHeader", jsonCase: "eth2"} ); -/** Same as `SignedBeaconBlockHeader` but slot is not bounded by the clock and must be a bigint */ -export const SignedBeaconBlockHeaderBigint = new ContainerType( +/** Same as `SignedBeaconBlockHeader` but slot is not bounded by the clock and must be a Uint8Array LE (instead of BigInt to improve performance) */ +export const SignedBeaconBlockHeaderBytes8 = new ContainerType( { - message: BeaconBlockHeaderBigint, + message: BeaconBlockHeaderBytes8, signature: BLSSignature, }, {typeName: "SignedBeaconBlockHeader", jsonCase: "eth2"} @@ -104,7 +104,7 @@ export const Checkpoint = new ContainerType( {typeName: "Checkpoint", jsonCase: "eth2"} ); -/** Checkpoint where epoch is NOT bounded by the clock, so must be a bigint */ +/** Checkpoint where epoch is NOT bounded by the clock, so must be a Uint8Array LE (instead of BigInt to improve performance) */ export const CheckpointBytes8 = new ContainerType( { epoch: Bytes8, @@ -330,7 +330,7 @@ export const AttesterSlashing = new ContainerType( { // In state transition, AttesterSlashing attestations are only partially validated. Their slot and epoch could // be higher than the clock and the slashing would still be valid. Same applies to attestation data index, which - // can be any arbitrary value. Must use bigint variants to hash correctly to all possible values + // can be any arbitrary value. Must use bytes8 variants to hash correctly to all possible values attestation1: IndexedAttestationBytes8, attestation2: IndexedAttestationBytes8, }, @@ -348,9 +348,9 @@ export const Deposit = new ContainerType( export const ProposerSlashing = new ContainerType( { // In state transition, ProposerSlashing headers are only partially validated. Their slot could be higher than the - // clock and the slashing would still be valid. Must use bigint variants to hash correctly to all possible values - signedHeader1: SignedBeaconBlockHeaderBigint, - signedHeader2: SignedBeaconBlockHeaderBigint, + // clock and the slashing would still be valid. Must use bytes8 variants to hash correctly to all possible values + signedHeader1: SignedBeaconBlockHeaderBytes8, + signedHeader2: SignedBeaconBlockHeaderBytes8, }, {typeName: "ProposerSlashing", jsonCase: "eth2"} ); diff --git a/packages/types/src/phase0/types.ts b/packages/types/src/phase0/types.ts index 05b21a19b92c..ebc2ac1c4cb7 100644 --- a/packages/types/src/phase0/types.ts +++ b/packages/types/src/phase0/types.ts @@ -3,9 +3,9 @@ import * as ssz from "./sszTypes.js"; export type AttestationSubnets = ValueOf; export type BeaconBlockHeader = ValueOf; -export type BeaconBlockHeaderBigint = ValueOf; +export type BeaconBlockHeaderBytes8 = ValueOf; export type SignedBeaconBlockHeader = ValueOf; -export type SignedBeaconBlockHeaderBigint = ValueOf; +export type SignedBeaconBlockHeaderBytes8 = ValueOf; export type Checkpoint = ValueOf; export type DepositMessage = ValueOf; export type DepositData = ValueOf; diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 20d826568c06..d6212493ccb3 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -71,7 +71,27 @@ export function intToBytesVanilla(value: bigint | number, length: number, endian } /** - * Convert byte array in LE to integer. + * Convert byte array to integer. + * If the value is greater than Number.MAX_SAFE_INTEGER, return Number.MAX_SAFE_INTEGER. + */ +export function bytesToIntOrMaxInt(value: Uint8Array, endianness: Endianness = "le"): number { + if (endianness === "le") { + // Buffer representation of Number.MAX_SAFE_INTEGER in le is + if (value.length >= 7 && value[6] > 0x1f) { + return Number.MAX_SAFE_INTEGER; + } + return bytesToInt(value, "le"); + } + + // Buffer representation of Number.MAX_SAFE_INTEGER in be is + if (value.length >= 7 && value[value.length - 7] > 0x1f) { + return Number.MAX_SAFE_INTEGER; + } + return bytesToInt(value, "be"); +} + +/** + * Convert byte array to integer. */ export function bytesToInt(value: Uint8Array, endianness: Endianness = "le"): number { // use Buffer api if possible since it's the fastest diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 3f4c7a523335..795bd790c90b 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -1,6 +1,7 @@ import "../setup.js"; import {assert, expect} from "chai"; -import {intToBytes, bytesToInt, compareBytesLe, intToBytesVanilla} from "../../src/index.js"; +import {intToBytes, bytesToInt, compareBytesLe, intToBytesVanilla, bytesToIntOrMaxInt} from "../../src/index.js"; +import {bigIntToBytes} from "../../lib/bytes.js"; describe("intToBytes", () => { const zeroedArray = (length: number): number[] => Array.from({length}, () => 0); @@ -61,6 +62,37 @@ describe("intToBytes", () => { }); }); +describe("bytesToIntOrMaxInt", () => { + const testCases: {input: Buffer; output: number}[] = [ + {input: Buffer.from([0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00]), output: Number.MAX_SAFE_INTEGER - 1}, + {input: Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00]), output: Number.MAX_SAFE_INTEGER}, + {input: Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00]), output: Number.MAX_SAFE_INTEGER}, + ]; + + for (const [i, {input, output}] of testCases.entries()) { + it(`Test case ${i}, Buffer to int - should produce ${output}`, () => { + expect(bytesToIntOrMaxInt(input)).to.be.equal(output); + }); + } + + const testCases2: {input: bigint; output: number}[] = [ + {input: BigInt(Number.MAX_SAFE_INTEGER) - BigInt(1), output: Number.MAX_SAFE_INTEGER - 1}, + {input: BigInt(Number.MAX_SAFE_INTEGER), output: Number.MAX_SAFE_INTEGER}, + {input: BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1), output: Number.MAX_SAFE_INTEGER}, + {input: BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1000), output: Number.MAX_SAFE_INTEGER}, + {input: BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10000), output: Number.MAX_SAFE_INTEGER}, + ]; + + for (const [i, {input, output}] of testCases2.entries()) { + it(`Test case ${i}, BigInt to int - should produce ${output}`, () => { + const bufferLE = bigIntToBytes(input, 8); + expect(bytesToIntOrMaxInt(bufferLE)).to.be.equal(output); + const bufferBE = bigIntToBytes(input, 8, "be"); + expect(bytesToIntOrMaxInt(bufferBE, "be")).to.be.equal(output); + }); + } +}); + describe("bytesToInt", () => { const testCases: {input: Buffer; output: number}[] = [ {input: Buffer.from([3]), output: 3}, From 73d59fca115c5298ff529ea0cc77a010dec47743 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 21 Nov 2023 17:05:52 +0700 Subject: [PATCH 8/9] fix: intToBytes import part in state-transition perf test --- packages/state-transition/test/perf/block/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/state-transition/test/perf/block/util.ts b/packages/state-transition/test/perf/block/util.ts index acc1838cfd1e..f8281ec74f07 100644 --- a/packages/state-transition/test/perf/block/util.ts +++ b/packages/state-transition/test/perf/block/util.ts @@ -4,7 +4,7 @@ import {BitArray} from "@chainsafe/ssz"; import {altair, phase0, ssz} from "@lodestar/types"; import {DOMAIN_DEPOSIT, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import {config} from "@lodestar/config/default"; -import {intToBytes} from "@lodestar/utils/src/bytes.js"; +import {intToBytes} from "@lodestar/utils"; import { computeDomain, computeEpochAtSlot, From fa3d4b9b4c0d2c2431f44e2645e68446d5b080e0 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 21 Nov 2023 19:55:26 +0700 Subject: [PATCH 9/9] fix: lint in types package --- packages/types/test/unit/ssz.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/test/unit/ssz.test.ts b/packages/types/test/unit/ssz.test.ts index 76645bbf4fae..2e948c1aea12 100644 --- a/packages/types/test/unit/ssz.test.ts +++ b/packages/types/test/unit/ssz.test.ts @@ -1,6 +1,6 @@ import {expect} from "chai"; -import {ssz} from "../../src/index.js"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ssz} from "../../src/index.js"; describe("size", function () { it("should calculate correct minSize and maxSize", () => {