Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export async function getHistoricalState(
for await (const block of db.blockArchive.valuesStream({gt: state.slot, lte: slot})) {
try {
state = state.stateTransition(
block,
config.getForkTypes(block.message.slot).SignedBeaconBlock.serialize(block),
false,
{
verifyProposer: false,
verifySignatures: false,
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/blocks/verifyBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export async function verifyBlocksInEpoch(
this.logger,
this.metrics,
this.validatorMonitor,
this.serializedCache,
abortController.signal,
opts
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
IBeaconStateView,
StateHashTreeRootSource,
} from "@lodestar/state-transition";
import {sszTypesFor} from "@lodestar/types";
import {ErrorAborted, Logger, byteArrayEquals} from "@lodestar/utils";
import {Metrics} from "../../metrics/index.js";
import {nextEventLoop} from "../../util/eventLoop.js";
import {SerializedCache} from "../../util/serializedCache.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {BlockProcessOpts} from "../options.js";
import {ValidatorMonitor} from "../validatorMonitor.js";
Expand All @@ -28,6 +30,7 @@ export async function verifyBlocksStateTransitionOnly(
logger: Logger,
metrics: Metrics | null,
validatorMonitor: ValidatorMonitor | null,
serializedCache: SerializedCache,
signal: AbortSignal,
opts: BlockProcessOpts & ImportBlockOpts
): Promise<{postStates: IBeaconStateView[]; proposerBalanceDeltas: number[]; verifyStateTime: number}> {
Expand All @@ -37,15 +40,20 @@ export async function verifyBlocksStateTransitionOnly(

for (let i = 0; i < blocks.length; i++) {
const {validProposerSignature, validSignatures} = opts;
const block = blocks[i].getBlock();
const blockInput = blocks[i];
const block = blockInput.getBlock();
const preState = i === 0 ? preState0 : postStates[i - 1];
const dataAvailabilityStatus = dataAvailabilityStatuses[i];

// STFN - per_slot_processing() + per_block_processing()
// NOTE: `regen.getPreState()` should have dialed forward the state already caching checkpoint states
const useBlsBatchVerify = !opts?.disableBlsBatchVerify;
const postState = preState.stateTransition(
block,
// We should have the serialized block from gossip in the hot path.
// Otherwise, fall back to serializing from block for the less latency critical paths like
// syncing/download.
serializedCache.get(block) ?? sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(block),
false,
{
// NOTE: Assume valid for now while sending payload to execution engine in parallel
// Latter verifyBlocksInEpoch() will make sure that payload is indeed valid
Expand Down
18 changes: 17 additions & 1 deletion packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isForkPostGloas,
} from "@lodestar/params";
import {
EMPTY_SIGNATURE,
EffectiveBalanceIncrements,
EpochShuffling,
IBeaconStateView,
Expand All @@ -34,6 +35,7 @@ import {
Root,
RootHex,
SignedBeaconBlock,
SignedBlindedBeaconBlock,
Slot,
Status,
UintNum64,
Expand Down Expand Up @@ -406,6 +408,7 @@ export class BeaconChain implements IBeaconChain {
blockStateCache,
checkpointStateCache,
seenBlockInputCache: this.seenBlockInputCache,
serializedCache: this.serializedCache,
db,
metrics,
validatorMonitor,
Expand Down Expand Up @@ -1110,7 +1113,20 @@ export class BeaconChain implements IBeaconChain {
body,
} as AssembledBlockType<T>;

const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block);
const isBlinded = isBlindedBeaconBlock(block);

// Build the serialized block and set signature to zero
// to re-use stateTransition() function which requires signed blocks
const serializedBlock = isBlinded
? this.config.getPostBellatrixForkTypes(slot).SignedBlindedBeaconBlock.serialize({
message: block as BlindedBeaconBlock,
signature: EMPTY_SIGNATURE,
} as SignedBlindedBeaconBlock)
: this.config.getForkTypes(slot).SignedBeaconBlock.serialize({
message: block as BeaconBlock,
signature: EMPTY_SIGNATURE,
} as SignedBeaconBlock);
const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, serializedBlock, isBlinded);
block.stateRoot = newStateRoot;
const blockRoot =
produceResult.type === BlockType.Full
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
IBeaconStateView,
StateHashTreeRootSource,
} from "@lodestar/state-transition";
import {BeaconBlock, BlindedBeaconBlock, Gwei, Root} from "@lodestar/types";
import {ZERO_HASH} from "../../constants/index.js";
import {Gwei, Root} from "@lodestar/types";
import {Metrics} from "../../metrics/index.js";

/**
Expand All @@ -16,13 +15,12 @@ import {Metrics} from "../../metrics/index.js";
export function computeNewStateRoot(
metrics: Metrics | null,
state: IBeaconStateView,
block: BeaconBlock | BlindedBeaconBlock
blockBytes: Uint8Array,
isBlinded: boolean
): {newStateRoot: Root; proposerReward: Gwei; postState: IBeaconStateView} {
// Set signature to zero to re-use stateTransition() function which requires the SignedBeaconBlock type
const blockEmptySig = {message: block, signature: ZERO_HASH};

const postState = state.stateTransition(
blockEmptySig,
blockBytes,
isBlinded,
{
// ExecutionPayloadStatus.valid: Assume payload valid, it has been produced by a trusted EL
executionPayloadStatus: ExecutionPayloadStatus.valid,
Expand Down
6 changes: 5 additions & 1 deletion packages/beacon-node/src/chain/regen/regen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {Logger, fromHex, toRootHex} from "@lodestar/utils";
import {IBeaconDb} from "../../db/index.js";
import {Metrics} from "../../metrics/index.js";
import {nextEventLoop} from "../../util/eventLoop.js";
import {SerializedCache} from "../../util/serializedCache.js";
import {getCheckpointFromState} from "../blocks/utils/checkpoint.js";
import {ChainEvent, ChainEventEmitter} from "../emitter.js";
import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js";
Expand All @@ -28,6 +29,7 @@ export type RegenModules = {
blockStateCache: BlockStateCache;
checkpointStateCache: CheckpointStateCache;
seenBlockInputCache: SeenBlockInput;
serializedCache: SerializedCache;
config: ChainForkConfig;
emitter: ChainEventEmitter;
logger: Logger;
Expand Down Expand Up @@ -235,7 +237,9 @@ export class StateRegenerator implements IStateRegeneratorInternal {
// We are only running the state transition to get a specific state's data.
// stateTransition() does the clone() inside, transfer cache to make the regen faster
state = state.stateTransition(
block,
this.modules.serializedCache.get(block) ??
this.modules.config.getForkTypes(block.message.slot).SignedBeaconBlock.serialize(block),
false,
{
// Replay previously imported blocks, assume valid and available
executionPayloadStatus: ExecutionPayloadStatus.valid,
Expand Down
31 changes: 18 additions & 13 deletions packages/beacon-node/test/spec/presets/finality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {getConfig} from "@lodestar/config/test-utils";
import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {
BeaconStateAllForks,
BeaconStateView,
DataAvailabilityStatus,
ExecutionPayloadStatus,
stateTransition,
} from "@lodestar/state-transition";
import {altair, bellatrix, ssz} from "@lodestar/types";
import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
Expand All @@ -18,24 +18,29 @@ import {RunnerType, TestRunnerFn, shouldVerify} from "../utils/types.js";
const finality: TestRunnerFn<FinalityTestCase, BeaconStateAllForks> = (fork) => {
return {
testFunction: (testcase) => {
let state = createCachedBeaconStateTest(testcase.pre, getConfig(fork));
let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, getConfig(fork)));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock;

state = stateTransition(state, signedBlock, {
// Should assume payload valid and blob data available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: false,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
});
state = state.stateTransition(
state.cachedState.config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock),
Comment on lines +21 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid accessing the internal cachedState property of BeaconStateView to retrieve the config. Since getConfig(fork) is already called, we can store the config in a local variable and use it directly.

Suggested change
let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, getConfig(fork)));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock;
state = stateTransition(state, signedBlock, {
// Should assume payload valid and blob data available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: false,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
});
state = state.stateTransition(
state.cachedState.config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock),
const config = getConfig(fork);
let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, config));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock;
state = state.stateTransition(
config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock),

false,
{
// Should assume payload valid and blob data available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: false,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
},
{}
) as BeaconStateView;
}

state.commit();
return state;
state.cachedState.commit();
return state.cachedState;
},
options: {
inputTypes: inputTypeSszTreeViewDU,
Expand Down
32 changes: 20 additions & 12 deletions packages/beacon-node/test/spec/presets/sanity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {InputType} from "@lodestar/spec-test-util";
import {
BeaconStateAllForks,
BeaconStateView,
DataAvailabilityStatus,
ExecutionPayloadStatus,
processSlots,
stateTransition,
} from "@lodestar/state-transition";
import {SignedBeaconBlock, deneb, ssz} from "@lodestar/types";
import {bnToNum} from "@lodestar/utils";
Expand Down Expand Up @@ -60,21 +60,29 @@ const sanityBlocks: TestRunnerFn<SanityBlocksTestCase, BeaconStateAllForks> = (f
return {
testFunction: (testcase) => {
const stateTB = testcase.pre;
let wrappedState = createCachedBeaconStateTest(stateTB, getConfig(fork));
let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, getConfig(fork)));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock;
wrappedState = stateTransition(wrappedState, signedBlock, {
// Assume valid and available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: verify,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
});

wrappedState = wrappedState.stateTransition(
wrappedState.cachedState.config
.getForkTypes(signedBlock.message.slot)
.SignedBeaconBlock.serialize(signedBlock),
Comment on lines +63 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid accessing the internal cachedState property of BeaconStateView to retrieve the config. Since getConfig(fork) is already called, we can store the config in a local variable and use it directly.

Suggested change
let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, getConfig(fork)));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock;
wrappedState = stateTransition(wrappedState, signedBlock, {
// Assume valid and available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: verify,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
});
wrappedState = wrappedState.stateTransition(
wrappedState.cachedState.config
.getForkTypes(signedBlock.message.slot)
.SignedBeaconBlock.serialize(signedBlock),
const config = getConfig(fork);
let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, config));
const verify = shouldVerify(testcase);
for (let i = 0; i < testcase.meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock;
wrappedState = wrappedState.stateTransition(
config
.getForkTypes(signedBlock.message.slot)
.SignedBeaconBlock.serialize(signedBlock),

false,
{
// Assume valid and available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: verify,
verifyProposer: verify,
verifySignatures: verify,
assertCorrectProgressiveBalances,
},
{}
) as BeaconStateView;
}
return wrappedState;
return wrappedState.cachedState;
},
options: {
inputTypes: inputTypeSszTreeViewDU,
Expand Down
30 changes: 18 additions & 12 deletions packages/beacon-node/test/spec/presets/transition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {config} from "@lodestar/config/default";
import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {
BeaconStateAllForks,
BeaconStateView,
DataAvailabilityStatus,
ExecutionPayloadStatus,
stateTransition,
} from "@lodestar/state-transition";
import {SignedBeaconBlock, ssz} from "@lodestar/types";
import {bnToNum} from "@lodestar/utils";
Expand Down Expand Up @@ -51,20 +51,26 @@ const transition =
const forkEpoch = bnToNum(meta.fork_epoch);
const testConfig = createChainForkConfig(getTransitionConfig(forkNext, forkEpoch));

let state = createCachedBeaconStateTest(testcase.pre, testConfig);
let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, testConfig));
for (let i = 0; i < meta.blocks_count; i++) {
const signedBlock = testcase[`blocks_${i}`] as SignedBeaconBlock;
state = stateTransition(state, signedBlock, {
// Assume valid and available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: true,
verifyProposer: false,
verifySignatures: false,
assertCorrectProgressiveBalances,
});

state = state.stateTransition(
testConfig.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock),
false,
{
// Assume valid and available for this test
executionPayloadStatus: ExecutionPayloadStatus.valid,
dataAvailabilityStatus: DataAvailabilityStatus.Available,
verifyStateRoot: true,
verifyProposer: false,
verifySignatures: false,
assertCorrectProgressiveBalances,
},
{}
) as BeaconStateView;
}
return state;
return state.cachedState;
},
options: {
inputTypes: inputTypeSszTreeViewDU,
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/test/spec/utils/gossipValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ function computePostState(
fork: ForkName
): IBeaconStateView {
return parentState.stateTransition(
signedBlock,
sszTypesFor(fork).SignedBeaconBlock.serialize(signedBlock),
false,
{
verifyStateRoot: true,
verifyProposer: true,
Expand Down
24 changes: 21 additions & 3 deletions packages/state-transition/src/stateTransition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {Epoch, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, ssz} from "@lodestar/types";
import {Epoch, Slot, ssz} from "@lodestar/types";
import {toRootHex} from "@lodestar/utils";
import {BlockExternalData, DataAvailabilityStatus, ExecutionPayloadStatus} from "./block/externalData.js";
import {processBlock} from "./block/index.js";
Expand Down Expand Up @@ -83,7 +83,8 @@ export enum StateHashTreeRootSource {
*/
export function stateTransition(
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock,
signedBlockBytes: Uint8Array,
isBlinded: boolean,
options: StateTransitionOpts = {
// Assume default to be valid and available
executionPayloadStatus: ExecutionPayloadStatus.valid,
Expand All @@ -93,8 +94,11 @@ export function stateTransition(
): CachedBeaconStateAllForks {
const {verifyStateRoot = true, verifyProposer = true} = options;

const blockSlot = readSignedBlockSlot(signedBlockBytes);
const signedBlock = isBlinded
? state.config.getPostBellatrixForkTypes(blockSlot).SignedBlindedBeaconBlock.deserialize(signedBlockBytes)
: state.config.getForkTypes(blockSlot).SignedBeaconBlock.deserialize(signedBlockBytes);
const block = signedBlock.message;
const blockSlot = block.slot;

// .clone() before mutating state in state transition
let postState = state.clone(options.dontTransferCache);
Expand Down Expand Up @@ -154,6 +158,20 @@ export function stateTransition(
return postState;
}

function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot {
if (signedBlockBytes.byteLength < 12) {
throw Error("Invalid signed block bytes: too short to read message offset and slot");
}

const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength);
const messageOffset = view.getUint32(0, true);
if (messageOffset + 8 > signedBlockBytes.byteLength) {
throw Error("Invalid signed block bytes: message offset out of range");
}

return Number(view.getBigUint64(messageOffset, true));
}
Comment on lines +161 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The minimum valid length of a serialized SignedBeaconBlock or SignedBlindedBeaconBlock is 108 bytes (100 bytes for the fixed part containing the 4-byte offset of message and the 96-byte signature, plus at least 8 bytes for the slot field of the message container).

Checking signedBlockBytes.byteLength < 12 is too permissive and can lead to out-of-bounds reads or reading garbage data if the input is malformed.

Additionally, the messageOffset (which is the offset of the first variable-sized field message) must be exactly 100. If it is not 100, the bytes are invalid and we should throw an error immediately to prevent reading arbitrary offsets.

Suggested change
function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot {
if (signedBlockBytes.byteLength < 12) {
throw Error("Invalid signed block bytes: too short to read message offset and slot");
}
const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength);
const messageOffset = view.getUint32(0, true);
if (messageOffset + 8 > signedBlockBytes.byteLength) {
throw Error("Invalid signed block bytes: message offset out of range");
}
return Number(view.getBigUint64(messageOffset, true));
}
function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot {
if (signedBlockBytes.byteLength < 108) {
throw Error("Invalid signed block bytes: too short to contain a valid signed block");
}
const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength);
const messageOffset = view.getUint32(0, true);
if (messageOffset !== 100) {
throw Error(`Invalid signed block bytes: message offset must be 100, got ${messageOffset}`);
}
return Number(view.getBigUint64(messageOffset, true));
}


/**
* Like `processSlots` from the spec but additionally handles fork upgrades
*
Expand Down
Loading
Loading