Skip to content
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
2 changes: 1 addition & 1 deletion packages/contracts/source/contracts/evm/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface Instance extends CommitHandler {
importAccountInfos(infos: AccountInfoExtended[]): Promise<void>;
importLegacyColdWallets(wallets: ImportLegacyColdWallet[]): Promise<void>;
getAccounts(offset: bigint, limit: bigint): Promise<GetAccountsResult>;
getLegacyAttributes(address: string, legacyAddress?: string): Promise<LegacyAttributes | undefined | null>;
getLegacyAttributes(address: string, legacyAddress?: string): Promise<LegacyAttributes | undefined>;
getLegacyColdWallets(offset: bigint, limit: bigint): Promise<GetLegacyColdWalletsResult>;
getReceipts(offset: bigint, limit: bigint): Promise<GetReceiptsResult>;
getReceiptsByBlockNumber(blockNumber: bigint): Promise<Record<string, TransactionReceipt>>;
Expand Down
10 changes: 5 additions & 5 deletions packages/contracts/source/contracts/evm/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export interface CommitStorageData {

export interface Storage {
getState(): Promise<{ blockNumber: number; totalRound: number }>;
getBlockHeaderData(blockNumber: number): Promise<BlockHeaderStorageData | undefined | null>;
getBlockNumberByHash(blockHash: string): Promise<number | undefined | null>;
getCommitData(blockNumber: number): Promise<CommitStorageData | undefined | null>;
getTransactionData(key: string): Promise<TransactionStorageData | undefined | null>;
getTransactionKeyByHash(txHash: string): Promise<string | undefined | null>;
getBlockHeaderData(blockNumber: number): Promise<BlockHeaderStorageData | undefined>;
getBlockNumberByHash(blockHash: string): Promise<number | undefined>;
getCommitData(blockNumber: number): Promise<CommitStorageData | undefined>;
getTransactionData(key: string): Promise<TransactionStorageData | undefined>;
getTransactionKeyByHash(txHash: string): Promise<string | undefined>;
isEmpty(): Promise<boolean>;
}
3 changes: 3 additions & 0 deletions packages/evm-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"@mainsail/crypto-commit": "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-signature-bls12-381": "workspace:*",
"@mainsail/crypto-signature-ecdsa": "workspace:*",
"@mainsail/crypto-transaction": "workspace:*",
"@mainsail/crypto-validation": "workspace:*",
Expand All @@ -45,6 +47,7 @@
"@mainsail/transactions": "workspace:*",
"@mainsail/validation": "workspace:*",
"@types/tmp": "0.2.6",
"esmock": "2.7.5",
"tmp": "0.2.5",
"uvu": "0.5.6",
"viem": "2.48.11"
Expand Down
106 changes: 106 additions & 0 deletions packages/evm-service/source/instances/evm-empty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Contracts } from "@mainsail/contracts";
import { Application } from "@mainsail/kernel";
import { setGracefulCleanup } from "tmp";
import { zeroHash } from "viem";

import { describe } from "@mainsail/test-runner";
import { wallets } from "../../test/fixtures/wallets";
import { prepareSandbox } from "../../test/helpers/prepare-sandbox";
import { EvmInstance } from "./evm";

// A freshly resolved instance with no genesis and nothing committed: every read should return
// its "empty" variant (zero / empty / undefined).
describe<{
app: Application;
instance: Contracts.Evm.Instance & Contracts.Evm.Storage;
}>("EvmInstance - empty", ({ assert, afterAll, afterEach, beforeEach, it }) => {
afterAll(() => setGracefulCleanup());

afterEach(async ({ instance }) => {
await instance.dispose();
});

beforeEach(async (context) => {
await prepareSandbox(context);
context.instance = context.app.resolve(EvmInstance);
});

it("isEmpty is true", async ({ instance }) => {
assert.true(await instance.isEmpty());
});

it("getState reports block number 0 and total round 0", async ({ instance }) => {
assert.equal(await instance.getState(), { blockNumber: 0, totalRound: 0 });
});

it("getAccountInfo reports a zero balance and nonce for an unknown account", async ({ instance }) => {
const [wallet] = wallets;

assert.equal(await instance.getAccountInfo(wallet.address), { balance: 0n, nonce: 0n });
});

it("getAccountInfoExtended reports a zero balance and nonce for an unknown account", async ({ instance }) => {
const [wallet] = wallets;

assert.equal(await instance.getAccountInfoExtended(wallet.address), {
balance: 0n,
nonce: 0n,
legacyAttributes: {},
address: wallet.address,
});
});

it("getAccounts is empty", async ({ instance }) => {
const { accounts } = await instance.getAccounts(0n, 100n);

assert.equal(accounts, []);
});

it("getLegacyColdWallets is empty", async ({ instance }) => {
const { wallets: coldWallets } = await instance.getLegacyColdWallets(0n, 100n);

assert.equal(coldWallets, []);
});

it("getLegacyAttributes returns nothing", async ({ instance }) => {
const [wallet] = wallets;

assert.undefined(await instance.getLegacyAttributes(wallet.address));
});

it("getReceipts is empty", async ({ instance }) => {
const { receipts } = await instance.getReceipts(0n, 100n);

assert.equal(receipts, []);
});

it("getReceiptsByBlockNumber is an empty record", async ({ instance }) => {
assert.equal(await instance.getReceiptsByBlockNumber(0n), {});
});

it("getBlockNumberByHash returns undefined", async ({ instance }) => {
assert.undefined(await instance.getBlockNumberByHash(zeroHash));
});

it("getBlockHeaderData returns nothing", async ({ instance }) => {
assert.undefined(await instance.getBlockHeaderData(0));
});

it("getCommitData returns undefined", async ({ instance }) => {
assert.undefined(await instance.getCommitData(0));
});

it("getTransactionData returns nothing", async ({ instance }) => {
assert.undefined(await instance.getTransactionData(zeroHash));
});

it("getTransactionKeyByHash returns nothing", async ({ instance }) => {
assert.undefined(await instance.getTransactionKeyByHash(zeroHash));
});

it("codeAt returns empty code", async ({ instance }) => {
const [wallet] = wallets;

assert.equal(await instance.codeAt(wallet.address), "0x");
});
});
213 changes: 213 additions & 0 deletions packages/evm-service/source/instances/evm-genesis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { Enums } from "@mainsail/constants";
import type { Contracts } from "@mainsail/contracts";
import { Application } from "@mainsail/kernel";
import { setGracefulCleanup } from "tmp";
import { zeroHash } from "viem";

import { describe } from "@mainsail/test-runner";
import { commitGenesis, processGenesis } from "../../test/helpers/commit-genesis";
import { prepareSandbox } from "../../test/helpers/prepare-sandbox";
import { EvmInstance } from "./evm";

// The real devnet genesis commit applied once. Every read should now reflect the committed
// block, its transactions, receipts and funded accounts. The reads don't mutate state, so a
// single committed instance is shared across the suite.
describe<{
app: Application;
instance: Contracts.Evm.Instance & Contracts.Evm.Storage;
genesisCommit: Contracts.Crypto.Commit;
}>("EvmInstance - genesis", ({ assert, afterAll, beforeAll, it }) => {
beforeAll(async (context) => {
await prepareSandbox(context);
context.instance = context.app.resolve(EvmInstance);
context.genesisCommit = await commitGenesis(context.app, context.instance);
});

afterAll(async (context) => {
await context.instance.dispose();
setGracefulCleanup();
});

it("isEmpty is false", async ({ instance }) => {
assert.false(await instance.isEmpty());
});

it("getState reports block number 0 and an increased total round", async ({ instance }) => {
assert.equal(await instance.getState(), { blockNumber: 0, totalRound: 1 });
});

it("getBlockNumberByHash resolves the genesis hash to block 0", async ({ instance, genesisCommit }) => {
assert.equal(await instance.getBlockNumberByHash(genesisCommit.block.hash), 0);
});

it("getBlockNumberByHash still returns undefined for an unknown hash", async ({ instance }) => {
assert.undefined(await instance.getBlockNumberByHash(zeroHash));
});

it("getBlockHeaderData returns the genesis header", async ({ instance, genesisCommit }) => {
const header = await instance.getBlockHeaderData(0);

assert.defined(header);
assert.equal(header!.hash, genesisCommit.block.hash);
});

it("getCommitData returns the genesis commit", async ({ instance }) => {
assert.defined(await instance.getCommitData(0));
});

it("exposes every committed transaction by hash and key", async ({ instance, genesisCommit }) => {
for (const transaction of genesisCommit.block.transactions) {
const key = await instance.getTransactionKeyByHash(transaction.hash);
assert.defined(key);
assert.defined(await instance.getTransactionData(key!));
}
});

it("stores one receipt per committed transaction", async ({ instance, genesisCommit }) => {
const receipts = await instance.getReceiptsByBlockNumber(0n);

assert.equal(Object.keys(receipts).length, genesisCommit.block.transactions.length);
});

it("getReceipts returns the committed receipts", async ({ instance, genesisCommit }) => {
const { receipts } = await instance.getReceipts(0n, BigInt(genesisCommit.block.transactions.length + 1));

assert.equal(receipts.length, genesisCommit.block.transactions.length);
});

it("getReceipt returns a single committed receipt by hash", async ({ instance, genesisCommit }) => {
const [transaction] = genesisCommit.block.transactions;

assert.defined(await instance.getReceipt(0n, transaction.hash));
});

it("funds the genesis proposer account", async ({ instance, genesisCommit }) => {
const info = await instance.getAccountInfo(genesisCommit.block.proposer);

assert.true(info.balance > 0n);
});

it("getAccounts returns the committed accounts", async ({ instance }) => {
const { accounts } = await instance.getAccounts(0n, 1000n);

assert.equal(accounts.length, 55); // initial wallet, validators 0x1;
});

it("getLegacyColdWallets is empty", async ({ instance }) => {
const { wallets: coldWallets } = await instance.getLegacyColdWallets(0n, 100n);

assert.equal(coldWallets, []);
});

// stateRoot/logsBloom operate on the pending commit, which the shared (already-committed)
// instance no longer has, so this runs the genesis up to — but not through — the commit.
it("computes the stateRoot and logsBloom of the pending genesis commit", async () => {
const context = {} as { app: Application };
await prepareSandbox(context);
const instance = context.app.resolve<Contracts.Evm.Instance & Contracts.Evm.Storage>(EvmInstance);

try {
const { genesisCommit, commitKey } = await processGenesis(context.app, instance);

const stateRoot = await instance.stateRoot(commitKey, genesisCommit.block.parentHash);
assert.string(stateRoot);
assert.length(stateRoot, 64);
// Genesis funds accounts, so the state is non-empty.
assert.false(stateRoot === "0".repeat(64));

const logsBloom = await instance.logsBloom(commitKey);
assert.string(logsBloom);
assert.length(logsBloom, 512);
} finally {
await instance.dispose();
}
});

// updateRewardsAndVotes mutates state (credits the proposer + calls the validator contract),
// so it runs on its own instance.
it("updateRewardsAndVotes credits the proposer with the block reward", async () => {
const context = {} as { app: Application };
await prepareSandbox(context);
const instance = context.app.resolve<Contracts.Evm.Instance & Contracts.Evm.Storage>(EvmInstance);

try {
const proposer = "0x1111111111111111111111111111111111111111";
const reward = 2_000_000_000n;
const commitKey = { blockNumber: 0n, round: 0n };

// genesis sets the deployer/validator contract addresses updateRewardsAndVotes needs.
await instance.initializeGenesis({
account: proposer,
deployerAccount: "0x0000000000000000000000000000000000000001",
initialBlockNumber: 0n,
initialSupply: 0n,
usernameContract: "0x0000000000000000000000000000000000000001",
validatorContract: "0x0000000000000000000000000000000000000001",
});

// Before: the proposer is unfunded.
assert.equal((await instance.getAccountInfo(proposer)).balance, 0n);

await instance.prepareNextCommit({ commitKey });
await instance.updateRewardsAndVotes({
blockReward: reward,
commitKey,
specId: Enums.Evm.SpecId.SHANGHAI,
timestamp: 12_345n,
validatorAddress: proposer,
});
await instance.onCommit({
blockNumber: 0n,
getBlock: () => ({ number: 0n, round: 0n }),
round: 0n,
setAccountUpdates: () => {},
} as any);

// After: the block reward has been credited to the proposer.
assert.equal((await instance.getAccountInfo(proposer)).balance, reward);
} finally {
await instance.dispose();
}
});

it("calculateRoundValidators runs after updateRewardsAndVotes", async () => {
const context = {} as { app: Application };
await prepareSandbox(context);
const instance = context.app.resolve<Contracts.Evm.Instance & Contracts.Evm.Storage>(EvmInstance);

try {
const proposer = "0x1111111111111111111111111111111111111111";
const commitKey = { blockNumber: 0n, round: 0n };

await instance.initializeGenesis({
account: proposer,
deployerAccount: "0x0000000000000000000000000000000000000001",
initialBlockNumber: 0n,
initialSupply: 0n,
usernameContract: "0x0000000000000000000000000000000000000001",
validatorContract: "0x0000000000000000000000000000000000000001",
});

await instance.prepareNextCommit({ commitKey });
await instance.updateRewardsAndVotes({
blockReward: 0n,
commitKey,
specId: Enums.Evm.SpecId.SHANGHAI,
timestamp: 12_345n,
validatorAddress: proposer,
});

await assert.resolves(() =>
instance.calculateRoundValidators({
commitKey,
roundValidators: 0n,
specId: Enums.Evm.SpecId.SHANGHAI,
timestamp: 12_345n,
validatorAddress: proposer,
}),
);
} finally {
await instance.dispose();
}
});
});
Loading
Loading