Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring sequencing modules #251

Merged
merged 7 commits into from
Jan 28, 2025
Merged
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 @@ -27,7 +27,8 @@ import { CachedStateService } from "../../../state/state/CachedStateService";
import { MessageStorage } from "../../../storage/repositories/MessageStorage";
import { Database } from "../../../storage/Database";

import { TransactionExecutionService } from "./TransactionExecutionService";
import { BlockProductionService } from "./BlockProductionService";
import { BlockResultService } from "./BlockResultService";

export interface BlockConfig {
allowEmptyBlock?: boolean;
Expand All @@ -49,7 +50,8 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
private readonly blockQueue: BlockQueue,
@inject("BlockTreeStore")
private readonly blockTreeStore: AsyncMerkleTreeStore,
private readonly executionService: TransactionExecutionService,
private readonly productionService: BlockProductionService,
private readonly resultService: BlockResultService,
@inject("MethodIdResolver")
private readonly methodIdResolver: MethodIdResolver,
@inject("Runtime") private readonly runtime: Runtime<RuntimeModulesRecord>,
Expand Down Expand Up @@ -107,7 +109,7 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {

public async generateMetadata(block: Block): Promise<BlockResult> {
const { result, blockHashTreeStore, treeStore } =
await this.executionService.generateMetadataForNextBlock(
await this.resultService.generateMetadataForNextBlock(
block,
this.unprovenMerkleStore,
this.blockTreeStore
Expand Down Expand Up @@ -212,7 +214,7 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
this.unprovenStateService
);

const block = await this.executionService.createBlock(
const block = await this.productionService.createBlock(
cachedStateService,
txs,
metadata,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { inject, injectable, Lifecycle, scoped } from "tsyringe";
import {
DefaultProvableHashList,
MandatoryProtocolModulesRecord,
MinaActions,
MinaActionsHashList,
NetworkState,
Protocol,
ProtocolModulesRecord,
ProvableBlockHook,
} from "@proto-kit/protocol";
import { Field } from "o1js";
import { log } from "@proto-kit/common";

import {
Block,
BlockWithResult,
TransactionExecutionResult,
} from "../../../storage/model/Block";
import { CachedStateService } from "../../../state/state/CachedStateService";
import { PendingTransaction } from "../../../mempool/PendingTransaction";

import { TransactionExecutionService } from "./TransactionExecutionService";

@injectable()
@scoped(Lifecycle.ContainerScoped)
export class BlockProductionService {
private readonly blockHooks: ProvableBlockHook<unknown>[];

public constructor(
@inject("Protocol")
protocol: Protocol<MandatoryProtocolModulesRecord & ProtocolModulesRecord>,
private readonly transactionExecutionService: TransactionExecutionService
) {
this.blockHooks =
protocol.dependencyContainer.resolveAll("ProvableBlockHook");
}

/**
* Main entry point for creating a unproven block with everything
* attached that is needed for tracing
*/
public async createBlock(
stateService: CachedStateService,
transactions: PendingTransaction[],
lastBlockWithResult: BlockWithResult,
allowEmptyBlocks: boolean
): Promise<Block | undefined> {
const lastResult = lastBlockWithResult.result;
const lastBlock = lastBlockWithResult.block;
const executionResults: TransactionExecutionResult[] = [];

const transactionsHashList = new DefaultProvableHashList(Field);
const eternalTransactionsHashList = new DefaultProvableHashList(
Field,
Field(lastBlock.toEternalTransactionsHash)
);

const incomingMessagesList = new MinaActionsHashList(
Field(lastBlock.toMessagesHash)
);

// Get used networkState by executing beforeBlock() hooks
const networkState = await this.blockHooks.reduce<Promise<NetworkState>>(
async (reduceNetworkState, hook) =>
await hook.beforeBlock(await reduceNetworkState, {
blockHashRoot: Field(lastResult.blockHashRoot),
eternalTransactionsHash: lastBlock.toEternalTransactionsHash,
stateRoot: Field(lastResult.stateRoot),
transactionsHash: Field(0),
networkStateHash: lastResult.afterNetworkState.hash(),
incomingMessagesHash: lastBlock.toMessagesHash,
}),
Promise.resolve(lastResult.afterNetworkState)
);

for (const tx of transactions) {
try {
// Create execution trace
const executionTrace =
// eslint-disable-next-line no-await-in-loop
await this.transactionExecutionService.createExecutionTrace(
stateService,
tx,
networkState
);

// Push result to results and transaction onto bundle-hash
executionResults.push(executionTrace);
if (!tx.isMessage) {
transactionsHashList.push(tx.hash());
eternalTransactionsHashList.push(tx.hash());
} else {
const actionHash = MinaActions.actionHash(
tx.toRuntimeTransaction().hashData()
);

incomingMessagesList.push(actionHash);
}
} catch (error) {
if (error instanceof Error) {
log.error("Error in inclusion of tx, skipping", error);
}
}
}

const previousBlockHash =
lastResult.blockHash === 0n ? undefined : Field(lastResult.blockHash);

if (executionResults.length === 0 && !allowEmptyBlocks) {
log.info(
"After sequencing, block has no sequencable transactions left, skipping block"
);
return undefined;
}

const block: Omit<Block, "hash"> = {
transactions: executionResults,
transactionsHash: transactionsHashList.commitment,
fromEternalTransactionsHash: lastBlock.toEternalTransactionsHash,
toEternalTransactionsHash: eternalTransactionsHashList.commitment,
height:
lastBlock.hash.toBigInt() !== 0n ? lastBlock.height.add(1) : Field(0),
fromBlockHashRoot: Field(lastResult.blockHashRoot),
fromMessagesHash: lastBlock.toMessagesHash,
toMessagesHash: incomingMessagesList.commitment,
previousBlockHash,

networkState: {
before: new NetworkState(lastResult.afterNetworkState),
during: networkState,
},
};

const hash = Block.hash(block);

return {
...block,
hash,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Bool, Field, Poseidon } from "o1js";
import { RollupMerkleTree } from "@proto-kit/common";
import {
BlockHashMerkleTree,
BlockHashTreeEntry,
BlockProverState,
MandatoryProtocolModulesRecord,
NetworkState,
Protocol,
ProtocolModulesRecord,
ProvableBlockHook,
RuntimeTransaction,
} from "@proto-kit/protocol";
import { inject, injectable, Lifecycle, scoped } from "tsyringe";

import {
Block,
BlockResult,
TransactionExecutionResult,
} from "../../../storage/model/Block";
import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore";
import { CachedMerkleTreeStore } from "../../../state/merkle/CachedMerkleTreeStore";
import { UntypedStateTransition } from "../helpers/UntypedStateTransition";
import type { StateRecord } from "../BatchProducerModule";

import { executeWithExecutionContext } from "./TransactionExecutionService";

function collectStateDiff(
stateTransitions: UntypedStateTransition[]
): StateRecord {
return stateTransitions.reduce<Record<string, Field[] | undefined>>(
(state, st) => {
if (st.toValue.isSome.toBoolean()) {
state[st.path.toString()] = st.toValue.value;
}
return state;
},
{}
);
}

function createCombinedStateDiff(transactions: TransactionExecutionResult[]) {
// Flatten diff list into a single diff by applying them over each other
return transactions
.map((tx) => {
const transitions = tx.protocolTransitions.concat(
tx.status.toBoolean() ? tx.stateTransitions : []
);
return collectStateDiff(transitions);
})
.reduce<StateRecord>((accumulator, diff) => {
// accumulator properties will be overwritten by diff's values
return Object.assign(accumulator, diff);
}, {});
}

@injectable()
@scoped(Lifecycle.ContainerScoped)
export class BlockResultService {
private readonly blockHooks: ProvableBlockHook<unknown>[];

public constructor(
@inject("Protocol")
protocol: Protocol<MandatoryProtocolModulesRecord & ProtocolModulesRecord>
) {
this.blockHooks =
protocol.dependencyContainer.resolveAll("ProvableBlockHook");
}

public async generateMetadataForNextBlock(
block: Block,
merkleTreeStore: AsyncMerkleTreeStore,
blockHashTreeStore: AsyncMerkleTreeStore,
modifyTreeStore = true
): Promise<{
result: BlockResult;
treeStore: CachedMerkleTreeStore;
blockHashTreeStore: CachedMerkleTreeStore;
}> {
const combinedDiff = createCombinedStateDiff(block.transactions);

const inMemoryStore = new CachedMerkleTreeStore(merkleTreeStore);
const tree = new RollupMerkleTree(inMemoryStore);
const blockHashInMemoryStore = new CachedMerkleTreeStore(
blockHashTreeStore
);
const blockHashTree = new BlockHashMerkleTree(blockHashInMemoryStore);

await inMemoryStore.preloadKeys(Object.keys(combinedDiff).map(BigInt));

// In case the diff is empty, we preload key 0 in order to
// retrieve the root, which we need later
if (Object.keys(combinedDiff).length === 0) {
await inMemoryStore.preloadKey(0n);
}

// TODO This can be optimized a lot (we are only interested in the root at this step)
await blockHashInMemoryStore.preloadKey(block.height.toBigInt());

Object.entries(combinedDiff).forEach(([key, state]) => {
const treeValue = state !== undefined ? Poseidon.hash(state) : Field(0);
tree.setLeaf(BigInt(key), treeValue);
});

const stateRoot = tree.getRoot();
const fromBlockHashRoot = blockHashTree.getRoot();

const state: BlockProverState = {
stateRoot,
transactionsHash: block.transactionsHash,
networkStateHash: block.networkState.during.hash(),
eternalTransactionsHash: block.toEternalTransactionsHash,
blockHashRoot: fromBlockHashRoot,
incomingMessagesHash: block.toMessagesHash,
};

// TODO Set StateProvider for @state access to state
const context = {
networkState: block.networkState.during,
transaction: RuntimeTransaction.dummyTransaction(),
};

const executionResult = await executeWithExecutionContext(
async () =>
await this.blockHooks.reduce<Promise<NetworkState>>(
async (networkState, hook) =>
await hook.afterBlock(await networkState, state),
Promise.resolve(block.networkState.during)
),
context
);

const { stateTransitions, methodResult } = executionResult;

// Update the block hash tree with this block
blockHashTree.setLeaf(
block.height.toBigInt(),
new BlockHashTreeEntry({
blockHash: Poseidon.hash([block.height, state.transactionsHash]),
closed: Bool(true),
}).hash()
);
const blockHashWitness = blockHashTree.getWitness(block.height.toBigInt());
const newBlockHashRoot = blockHashTree.getRoot();

return {
result: {
afterNetworkState: methodResult,
stateRoot: stateRoot.toBigInt(),
blockHashRoot: newBlockHashRoot.toBigInt(),
blockHashWitness,

blockStateTransitions: stateTransitions.map((st) =>
UntypedStateTransition.fromStateTransition(st)
),
blockHash: block.hash.toBigInt(),
},
treeStore: inMemoryStore,
blockHashTreeStore: blockHashInMemoryStore,
};
}
}
Loading
Loading