Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
205a3ab
Extract transaction forger
sebastijankuzner May 7, 2026
80958e3
Extract block forger
sebastijankuzner May 7, 2026
923888b
Create forger package
sebastijankuzner May 7, 2026
6fd7ffd
Remove forger from validator
sebastijankuzner May 7, 2026
e9201b7
Merge branch 'develop' into refactor/validator/split-validator-class
sebastijankuzner May 7, 2026
4152a98
Update pnpm-lock.yaml
sebastijankuzner May 7, 2026
5f6cda4
Add forger to core
sebastijankuzner May 7, 2026
55fd3fd
Fix dependencies
sebastijankuzner May 7, 2026
6421aa7
Rename identifiers
sebastijankuzner May 7, 2026
f11f3cb
Move contracts
sebastijankuzner May 7, 2026
8eb734e
Export classes
sebastijankuzner May 7, 2026
f83fbc2
Fix validator tests
sebastijankuzner May 7, 2026
3f746ee
Merge branch 'develop' into refactor/validator/split-validator-class
sebastijankuzner May 7, 2026
ed6f971
style: resolve style guide violations [ci-lint-fix]
sebastijankuzner May 7, 2026
2825ec7
Update e2e
sebastijankuzner May 7, 2026
1264cad
Add tsconfig.test.tsbuildinfo to gitignore
sebastijankuzner May 7, 2026
0fde85f
Fix functional tests
sebastijankuzner May 7, 2026
e4ff466
Remove prepare block from validator
sebastijankuzner May 7, 2026
dbf4dda
Fix api-development
sebastijankuzner May 7, 2026
7c66396
Fix consensus tests
sebastijankuzner May 7, 2026
9b6ea93
Fix tests
sebastijankuzner May 7, 2026
7354976
style: resolve style guide violations [ci-lint-fix]
sebastijankuzner May 7, 2026
d412918
Remove defaults
sebastijankuzner May 7, 2026
73e0bf6
Fix deps
sebastijankuzner May 7, 2026
0088ffd
Fix configuration import
sebastijankuzner May 8, 2026
7b76898
Rename variable
sebastijankuzner May 8, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ bundle.min.js
packages/**/distribution/
packages/**/tsconfig.tsbuildinfo
!packages/kernel/test/stubs/**
tsconfig.test.tsbuildinfo

# Microsoft Visual Studio settings
.vs
Expand Down Expand Up @@ -141,4 +142,4 @@ tests/functional/**/paths/data/**
!tests/functional/**/paths/data/.gitkeep

.claude/worktrees
.claude/settings.local.json
.claude/settings.local.json
5 changes: 1 addition & 4 deletions packages/api-development/source/controllers/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ import { Controller } from "./controller.js";

@injectable()
export class ConsensusController extends Controller {
@inject(Identifiers.Consensus.Service)
private readonly consensus!: Contracts.Consensus.Service;

@inject(Identifiers.Consensus.RoundStateRepository)
private readonly roundStateRepository!: Contracts.Consensus.RoundStateRepository;

@inject(Identifiers.ValidatorSet.Service)
private readonly validatorSet!: Contracts.ValidatorSet.Service;

public async state(request: Types.HapiRequest): Promise<object> {
const state = this.consensus.getState();
const state = this.app.get<Contracts.Consensus.Service>(Identifiers.Consensus.Service).getState();

const roundStates = this.roundStateRepository.getRoundStates();

Expand Down
20 changes: 13 additions & 7 deletions packages/consensus/source/consensus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Context = {
roundState: Contracts.Consensus.RoundState;
roundStateRepository: any;
peerStatistic: any;
forger: any;
};

describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each }) => {
Expand Down Expand Up @@ -135,6 +136,10 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
newRound: () => {},
};

context.forger = {
forgeBlock: () => {},
};

context.app = new Application();

context.app.bind(Identifiers.Cryptography.Configuration).toConstantValue(context.cryptoConfiguration);
Expand All @@ -152,6 +157,7 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
context.app.bind(Identifiers.Consensus.RoundStateRepository).toConstantValue(context.roundStateRepository);
context.app.bind(Identifiers.Services.Log.Service).toConstantValue(context.logger);
context.app.bind(Identifiers.P2P.Statistic.Service).toConstantValue(context.peerStatistic);
context.app.bind(Identifiers.Forger.Block).toConstantValue(context.forger);

context.consensus = context.app.resolve(Consensus);
});
Expand Down Expand Up @@ -240,15 +246,15 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
validatorSet,
eventDispatcher,
scheduler,
forger,
}) => {
const validator = {
prepareBlock: () => {},
propose: () => {},
};

const spyScheduleClear = spy(scheduler, "clear");
const spyScheduleTimeoutBlockPrepare = spy(scheduler, "scheduleTimeoutBlockPrepare");
const spyValidatorPrepareBlock = stub(validator, "prepareBlock").resolvedValue(block);
const spyForgerForgeBlock = stub(forger, "forgeBlock").resolvedValue(block);
const spyValidatorPropose = stub(validator, "propose").resolvedValue(proposal);

const spyLoggerInfo = spy(logger, "info");
Expand All @@ -269,8 +275,8 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
spyGetRoundState.calledWith(1, 0);
spyGetValidator.calledOnce();
spyGetValidator.calledWith(proposer.blsPublicKey);
spyValidatorPrepareBlock.calledOnce();
spyValidatorPrepareBlock.calledWith(proposer.address, 0);
spyForgerForgeBlock.calledOnce();
spyForgerForgeBlock.calledWith(proposer.address, 0);
getValidatorIndexByWalletAddress.calledOnce();
getValidatorIndexByWalletAddress.calledWith(proposer.address);
spyValidatorPropose.calledOnce();
Expand Down Expand Up @@ -299,16 +305,16 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
validatorSet,
eventDispatcher,
scheduler,
forger,
}) => {
const validator = {
prepareBlock: () => {},
propose: () => {},
};

const spyScheduleClear = spy(scheduler, "clear");
const spyScheduleTimeoutBlockPrepare = spy(scheduler, "scheduleTimeoutBlockPrepare");

const spyValidatorPrepareBlock = stub(validator, "prepareBlock").resolvedValue(block);
const spyForgerForgeBlock = stub(forger, "forgeBlock").resolvedValue(block);
const spyValidatorPropose = stub(validator, "propose").resolvedValue(proposal);

const spyLoggerInfo = spy(logger, "info");
Expand Down Expand Up @@ -339,7 +345,7 @@ describe<Context>("Consensus", ({ it, beforeEach, assert, stub, spy, clock, each
spyGetRoundState.calledWith(1, 1);
spyGetValidator.calledOnce();
spyGetValidator.calledWith(proposer.blsPublicKey);
spyValidatorPrepareBlock.neverCalled();
spyForgerForgeBlock.neverCalled();
spyRoundStateAggregatePrevotes.calledOnce();
spyRoundStateGetBlock.calledOnce();
getValidatorIndexByWalletAddress.calledOnce();
Expand Down
5 changes: 4 additions & 1 deletion packages/consensus/source/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export class Consensus implements Contracts.Consensus.Service {
@inject(Identifiers.ValidatorSet.Service)
private readonly validatorSet!: Contracts.ValidatorSet.Service;

@inject(Identifiers.Forger.Block)
private readonly blockForger!: Contracts.Forger.BlockForger;

@inject(Identifiers.Services.EventDispatcher.Service)
private readonly eventDispatcher!: Contracts.Kernel.EventDispatcher;

Expand Down Expand Up @@ -524,7 +527,7 @@ export class Consensus implements Contracts.Consensus.Service {
);
}

this.#proposedBlock = this.#proposedBlock = await registeredProposer.prepareBlock(
this.#proposedBlock = this.#proposedBlock = await this.blockForger.forgeBlock(
roundState.proposer.address,
this.#round,
this.scheduler.getNextBlockTimestamp(this.#roundStartTime),
Expand Down
4 changes: 4 additions & 0 deletions packages/constants/source/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export const Identifiers = {
Factory: Symbol("Evm<WorkerSubprocess.Factory>"),
},
},
Forger: {
Block: Symbol("Forger<Block>"),
Transaction: Symbol("Forger<Transaction>"),
},
P2P: {
ApiNode: {
Discoverer: Symbol("P2P<ApiNode.Discoverer>"),
Expand Down
20 changes: 20 additions & 0 deletions packages/contracts/source/contracts/forger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Block, Transaction } from "./crypto/index.js";
import type { CommitKey } from "./evm/index.js";

export interface TransactionForger {
getTransactions(
generatorAddress: string,
timestamp: number,
commitKey: CommitKey,
): Promise<{
logsBloom: string;
stateRoot: string;
transactions: Transaction[];
gasUsed: number;
fee: bigint;
}>;
}

export interface BlockForger {
forgeBlock(generatorAddress: string, round: number, timestamp: number): Promise<Block>;
}
1 change: 1 addition & 0 deletions packages/contracts/source/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * as Consensus from "./consensus/index.js";
export * as Crypto from "./crypto/index.js";
export * as Database from "./database.js";
export * as Evm from "./evm/index.js";
export * as Forger from "./forger.js";
export * as Kernel from "./kernel/index.js";
export * as NetworkGenerator from "./network-generator.js";
export * as P2P from "./p2p/index.js";
Expand Down
1 change: 0 additions & 1 deletion packages/contracts/source/contracts/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export interface ValidatorKeyPair {
export interface Validator {
configure(keyPair: ValidatorKeyPair): Validator;
getConsensusPublicKey(): string;
prepareBlock(generatorAddress: string, round: number, timestamp: number): Promise<Block>;
propose(
validatorIndex: number,
round: number,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/bin/config/devnet/core/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
{
"package": "@mainsail/evm-consensus"
},
{
"package": "@mainsail/forger"
},
{
"package": "@mainsail/validator"
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@mainsail/evm-service": "workspace:*",
"@mainsail/evm-state": "workspace:*",
"@mainsail/exceptions": "workspace:*",
"@mainsail/forger": "workspace:*",
"@mainsail/logger-pino": "workspace:*",
"@mainsail/networking-dns": "workspace:*",
"@mainsail/networking-ntp": "workspace:*",
Expand Down
19 changes: 19 additions & 0 deletions packages/forger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Mainsail - Validator

![banner](https://raw.githubusercontent.com/ArkEcosystem/mainsail/main/banner.jpeg)

## Documentation

You can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://ark.dev/docs/mainsail).

## Security

If you discover a security vulnerability within this package, please send an e-mail to [security@ark.io](mailto:security@ark.io). All security vulnerabilities will be promptly addressed.

## Credits

This project exists thanks to all the people who [contribute](https://github.com/ArkEcosystem/mainsail/graphs/contributors).

## License

[GPL-3.0-only](https://github.com/ArkEcosystem/mainsail/blob/main/LICENSE) © [ARK Ecosystem](https://ark.io)
57 changes: 57 additions & 0 deletions packages/forger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@mainsail/forger",
"version": "0.0.1-rc.9",
"description": "Forging utils for the Mainsail blockchain",
"license": "GPL-3.0-only",
"contributors": [],
"type": "module",
"main": "distribution/index.js",
"types": "distribution/index.d.ts",
"files": [
"distribution"
],
"scripts": {
"build": "tsc -b",
"build:watch": "pnpm run clean && tsc -w",
"release": "pnpm publish --access public",
"test": "pnpm run uvu source .test.ts",
"test:coverage": "c8 -r=text -r=lcov --all pnpm run test",
"test:coverage:html": "c8 -r html --all pnpm run test",
"test:file": "pnpm run uvu source",
"uvu": "tsx --tsconfig ../../tsconfig.test.json ./node_modules/uvu/bin.js"
},
"dependencies": {
"@mainsail/constants": "workspace:*",
"@mainsail/container": "workspace:*",
"@mainsail/evm-consensus": "workspace:*",
"@mainsail/kernel": "workspace:*",
"@mainsail/utils": "workspace:*",
"joi": "18.1.2"
},
"devDependencies": {
"@mainsail/blockchain-utils": "workspace:*",
"@mainsail/contracts": "workspace:*",
"@mainsail/crypto-address-base58": "workspace:*",
"@mainsail/crypto-address-keccak256": "workspace:*",
"@mainsail/crypto-block": "workspace:*",
"@mainsail/crypto-config": "workspace:*",
"@mainsail/crypto-hash-bcrypto": "workspace:*",
"@mainsail/crypto-key-pair-bls12-381": "workspace:*",
"@mainsail/crypto-key-pair-ecdsa": "workspace:*",
"@mainsail/crypto-messages": "workspace:*",
"@mainsail/crypto-proposal": "workspace:*",
"@mainsail/crypto-signature-bls12-381": "workspace:*",
"@mainsail/crypto-signature-ecdsa": "workspace:*",
"@mainsail/crypto-transaction": "workspace:*",
"@mainsail/crypto-validation": "workspace:*",
"@mainsail/crypto-wif": "workspace:*",
"@mainsail/serializer": "workspace:*",
"@mainsail/test-runner": "workspace:*",
"@mainsail/transactions": "workspace:*",
"@mainsail/validation": "workspace:*",
"uvu": "0.5.6"
},
"engines": {
"node": ">=24"
}
}
95 changes: 95 additions & 0 deletions packages/forger/source/block-forger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Contracts } from "@mainsail/contracts";

import { Identifiers } from "@mainsail/constants";
import { inject, injectable } from "@mainsail/container";
import { assert } from "@mainsail/utils";

@injectable()
export class BlockForger implements Contracts.Forger.BlockForger {
@inject(Identifiers.Cryptography.Configuration)
private readonly cryptoConfiguration!: Contracts.Crypto.Configuration;

@inject(Identifiers.State.Store)
protected readonly stateStore!: Contracts.State.Store;

@inject(Identifiers.Cryptography.Block.Factory)
private readonly blockFactory!: Contracts.Crypto.BlockFactory;

@inject(Identifiers.Cryptography.Hash.Factory)
private readonly hashFactory!: Contracts.Crypto.HashFactory;

@inject(Identifiers.Forger.Transaction)
protected readonly transactionForger!: Contracts.Forger.TransactionForger;

@inject(Identifiers.BlockchainUtils.FeeCalculator)
protected readonly gasFeeCalculator!: Contracts.BlockchainUtils.FeeCalculator;

public async forgeBlock(
generatorAddress: string,
round: number,
timestamp: number,
): Promise<Contracts.Crypto.Block> {
const previousBlock = this.stateStore.getLastBlock();
const blockNumber = previousBlock.number + 1;

const { fee, gasUsed, logsBloom, stateRoot, transactions } = await this.transactionForger.getTransactions(
generatorAddress,
timestamp,
{
blockNumber: BigInt(blockNumber),
round: BigInt(round),
},
);
return this.#makeBlock(round, generatorAddress, logsBloom, stateRoot, transactions, timestamp, gasUsed, fee);
}

async #makeBlock(
round: number,
proposer: string,
logsBloom: string,
stateRoot: string,
transactions: Contracts.Crypto.Transaction[],
timestamp: number,
gasUsed: number,
fee: bigint,
): Promise<Contracts.Crypto.Block> {
const previousBlock = this.stateStore.getLastBlock();
const number = previousBlock.number + 1;
const milestone = this.cryptoConfiguration.getMilestone(number);

const payloadBuffers: Buffer[] = [];
const transactionData: Contracts.Crypto.TransactionData[] = [];

// The payload length needs to account for the overhead of each serialized transaction
// which is a uint32 per transaction to store the individual length.
let payloadSize = transactions.length * 4;

for (const transaction of transactions) {
assert.string(transaction.hash);

payloadBuffers.push(Buffer.from(transaction.hash, "hex"));
transactionData.push(transaction.toData());
payloadSize += transaction.serialized.length;
}

return this.blockFactory.make(
{
fee,
gasUsed,
logsBloom,
number,
parentHash: previousBlock.hash,
payloadSize,
proposer,
reward: BigInt(milestone.reward),
round,
stateRoot,
timestamp,
transactionsCount: transactionData.length,
transactionsRoot: this.hashFactory.sha256(payloadBuffers).toString("hex"),
version: 1,
},
transactions,
);
}
}
8 changes: 8 additions & 0 deletions packages/forger/source/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as index from "./index";
import { describe } from "@mainsail/test-runner";

describe("Index", ({ assert, it }) => {
it("should export ServiceProvider", () => {
assert.defined(index.ServiceProvider);
});
});
3 changes: 3 additions & 0 deletions packages/forger/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./block-forger.js";
export * from "./service-provider.js";
export * from "./transaction-forger.js";
Loading
Loading