test: port Moonwall block validation suite (#201)

This PR restructures and port the block validation test suite from
Moonbeam and add the necessary infrastructure for contract-based
testing.

### Test Suites Added

**Block Validation Suite 1** (`test-block-1.ts`) - *Refactored from
original `test-block.ts`*
- T01: Validates block number increments correctly after manual block
creation
- T02: Checks block timestamps are valid and within expected bounds
- T03: Verifies complete block structure including gasLimit, difficulty,
receiptsRoot, transactionsRoot, logsBloom, and other Ethereum-compatible
fields
- T04: Confirms blocks are retrievable by hash
- T05: Confirms blocks are retrievable by number

**Block Validation Suite 2** (`test-block-2.ts`) - *New*
- T01: Verifies block number persistence across test cases
- T02: Validates parent-child block hash linkage in the chain

**Block Gas Limits Suite** (`test-block-gas.ts`) - *New*
- T01-T06: Tests all three transaction types (legacy, eip1559, eip2930)
can deploy contracts at max extrinsic gas limit, and correctly reject
transactions exceeding that limit
- T07: Deploys `BlockVariables` contract and verifies runtime gas limit
is accessible from within contract execution

**Block Genesis Suite** (`test-block-genesis.ts`) - *New*
- T01: Validates genesis block (block 0) contains correct
Ethereum-compatible structure and empty transaction/uncle lists
- T02: Confirms genesis block is retrievable by hash

### Infrastructure Additions

**Contract Deployment Helpers**
- `fetchCompiledContract`: Loads compiled Solidity artifacts with ABI
and bytecode from JSON output
- `deployCompiledContract`: Handles contract deployment with support for
legacy, EIP-1559, and EIP-2930 transaction types, including gas
parameter configuration

**Solidity Test Fixtures**
- `BlockVariables.sol`: Exposes `block.gaslimit`, `block.chainid`,
`block.number` via view functions for runtime validation
- `Fibonacci.sol`: Provides `fib2(n)` pure function for computational
gas testing
- `MultiplyBy7.sol`: Minimal pure function contract for basic deployment
testing

**Enhanced Runtime Constants**
- Added `EXTRINSIC_GAS_LIMIT` (52M), `BLOCK_WEIGHT_LIMIT` (2T),
`MAX_POV_SIZE` (10MB) for all three environments
- Constants derived from and documented against Rust runtime
configuration in `operator/runtime/*/src/lib.rs`

### Dependencies
- Added `solc@0.8.30` and supporting packages (`yargs`,
`command-exists`, `js-sha3`, `memorystream`) for local Solidity
compilation
This commit is contained in:
Ahmad Kaouk 2025-10-06 09:47:35 +02:00 committed by GitHub
parent dc0f0673e2
commit f040682d93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 708 additions and 35 deletions

View file

@ -26,14 +26,17 @@
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"polkadot-api": "^1.15.1",
"solc": "^0.8.30",
"tiny-invariant": "^1.3.3",
"viem": "^2.31.3",
"wagmi": "^2.15.6",
"yaml": "^2.8.0",
"yargs": "^18.0.0",
"zod": "^3.25.67",
},
"devDependencies": {
"@types/bun": "latest",
"@types/yargs": "^17.0.33",
"@typescript/native-preview": "^7.0.0-dev.20250618.1",
},
"peerDependencies": {
@ -620,6 +623,10 @@
"@types/ws": ["@types/ws@8.5.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w=="],
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20250618.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250618.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250618.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20250618.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250618.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20250618.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250618.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20250618.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-FkisKblT8PEyq08bjVXND137ojhr+1CfRhSIwvzilMcnEwav2wzxEGZndLNSGzwUlqkS+xQCRx1ch8clpsLq9Q=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20250618.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h4fokGV0LQ+mCfc/Cpubn1m4OURNozMPQhzyI4pHOlGZdyZyYRj2e2LLzr3OwqeRnn71al2fcBfay63d/P3FSQ=="],
@ -886,6 +893,8 @@
"comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="],
"command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="],
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"comment-parser": ["comment-parser@1.4.1", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="],
@ -1274,6 +1283,8 @@
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@ -1344,6 +1355,8 @@
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"micro-ftch": ["micro-ftch@0.3.1", "", {}, "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="],
@ -1686,6 +1699,8 @@
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
"solc": ["solc@0.8.30", "", { "dependencies": { "command-exists": "^1.2.8", "commander": "^8.1.0", "follow-redirects": "^1.12.1", "js-sha3": "0.8.0", "memorystream": "^0.3.1", "semver": "^5.5.0", "tmp": "0.0.33" }, "bin": { "solcjs": "solc.js" } }, "sha512-9Srk/gndtBmoUbg4CE6ypAzPQlElv8ntbnl6SigUBAzgXKn35v87sj04uZeoZWjtDkdzT0qKFcIo/wl63UMxdw=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"sort-keys": ["sort-keys@5.1.0", "", { "dependencies": { "is-plain-obj": "^4.0.0" } }, "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ=="],
@ -2332,6 +2347,12 @@
"socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"solc/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"solc/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"solc/tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],

View file

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract BlockVariables {
uint256 public initialgaslimit;
uint256 public initialchainid;
uint256 public initialnumber;
constructor() {
initialgaslimit = block.gaslimit;
initialchainid = block.chainid;
initialnumber = block.number;
}
function getGasLimit() public view returns (uint256) {
return block.gaslimit;
}
function getChainId() public view returns (uint256) {
return block.chainid;
}
function getNumber() public view returns (uint256) {
return block.number;
}
}

View file

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract Fibonacci {
function fib2(uint256 n) public pure returns (uint256 b) {
if (n == 0) {
return 0;
}
uint256 a = 1;
b = 1;
for (uint256 i = 2; i < n; i++) {
uint256 c = a + b;
a = b;
b = c;
}
return b;
}
}

View file

@ -0,0 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract MultiplyBy7 {
function multiply(uint256 a) public pure returns (uint256 d) {
return a * 7;
}
}

View file

@ -32,15 +32,25 @@ class RuntimeConstant<T> {
}
// Fees and gas limits
// Values derived from Rust runtime configuration in operator/runtime/*/src/lib.rs
export const RUNTIME_CONSTANTS = {
"DATAHAVEN-STAGENET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
GAS_LIMIT: new RuntimeConstant(60_000_000n),
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
},
"DATAHAVEN-MAINNET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
GAS_LIMIT: new RuntimeConstant(60_000_000n),
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
},
"DATAHAVEN-TESTNET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
GAS_LIMIT: new RuntimeConstant(60_000_000n),
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
}
};

View file

@ -0,0 +1,64 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { Abi } from "viem";
interface ArtifactEvmBytecode {
object?: string;
}
interface ArtifactEvm {
bytecode?: ArtifactEvmBytecode;
}
interface ArtifactContract {
abi?: Abi;
evm?: ArtifactEvm;
bytecode?: `0x${string}`;
}
interface CompiledContractArtifactJson {
abi?: Abi;
byteCode?: `0x${string}`;
contract: ArtifactContract;
sourceCode: string;
}
export interface CompiledContractArtifact {
abi: Abi;
bytecode: `0x${string}`;
contract: ArtifactContract;
sourceCode: string;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const fetchCompiledContract = async (
contractName: string
): Promise<CompiledContractArtifact> => {
const artifactPath = path.join(__dirname, "../", "contracts", "out", `${contractName}.json`);
const artifactContent = await readFile(artifactPath, "utf-8");
const artifactJson = JSON.parse(artifactContent) as CompiledContractArtifactJson;
const abi = artifactJson.abi ?? artifactJson.contract.abi;
if (!abi) {
throw new Error(`Missing ABI for compiled contract: ${contractName}`);
}
const bytecodeFromContract = artifactJson.contract.bytecode;
const bytecodeObject = artifactJson.contract.evm?.bytecode?.object;
const bytecode =
bytecodeFromContract ??
(bytecodeObject ? (`0x${bytecodeObject}` as const) : artifactJson.byteCode);
if (!bytecode) {
throw new Error(`Missing bytecode for compiled contract: ${contractName}`);
}
return {
abi,
bytecode,
contract: artifactJson.contract,
sourceCode: artifactJson.sourceCode
} satisfies CompiledContractArtifact;
};

View file

@ -0,0 +1,66 @@
import type { Abi } from "viem";
import { fetchCompiledContract } from "./contracts";
export const TransactionTypes = ["legacy", "eip1559", "eip2930"] as const;
export interface DeployCompiledContractOptions {
type?: (typeof TransactionTypes)[number];
gas?: bigint;
gasPrice?: bigint;
maxFeePerGas?: bigint;
maxPriorityFeePerGas?: bigint;
value?: bigint;
}
export interface DeployCompiledContractResult {
hash: `0x${string}`;
contractAddress: `0x${string}` | null;
status: "success" | "reverted";
abi: Abi;
}
export const deployCompiledContract = async (
context: any,
contractName: string,
options: DeployCompiledContractOptions = {}
): Promise<DeployCompiledContractResult> => {
const artifact = await fetchCompiledContract(contractName);
const tx: any = {
data: artifact.bytecode,
gas: options.gas,
value: options.value ?? 0n
};
switch (options.type) {
case "legacy":
tx.type = "legacy";
if (options.gasPrice !== undefined) tx.gasPrice = options.gasPrice;
break;
case "eip2930":
tx.type = "eip2930";
if (options.gasPrice !== undefined) tx.gasPrice = options.gasPrice;
tx.accessList = [];
break;
case "eip1559":
default:
tx.type = "eip1559";
if (options.maxFeePerGas !== undefined) tx.maxFeePerGas = options.maxFeePerGas;
if (options.maxPriorityFeePerGas !== undefined)
tx.maxPriorityFeePerGas = options.maxPriorityFeePerGas;
break;
}
const hash: `0x${string}` = await context.viem().sendTransaction(tx);
if (typeof context.createBlock === "function") {
await context.createBlock();
}
const receipt = await context.viem().waitForTransactionReceipt({ hash });
return {
hash,
contractAddress: receipt.contractAddress,
status: receipt.status,
abi: artifact.abi
};
};

View file

@ -1 +1,2 @@
export * from "./constants";
export * from "./deploy";

View file

@ -1,6 +1,6 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { ConstantStore } from "../../helpers";
import { ConstantStore } from "../../../../helpers";
describeSuite({
id: "D010101",

View file

@ -0,0 +1,36 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "D010102",
title: "Block creation - suite 2",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let initialBlockNumber: bigint;
let postSetupBlockNumber: bigint;
beforeAll(async () => {
initialBlockNumber = await context.viem().getBlockNumber();
await context.createBlock();
postSetupBlockNumber = await context.viem().getBlockNumber();
});
it({
id: "T01",
title: "should be at block 2",
test: async () => {
expect(await context.viem().getBlockNumber()).to.equal(postSetupBlockNumber);
}
});
it({
id: "T02",
title: "should include previous block hash as parent",
test: async () => {
const block = await context.viem().getBlock({ blockTag: "latest" });
const previousBlock = await context.viem().getBlock({ blockNumber: initialBlockNumber });
expect(block.hash).to.not.equal(previousBlock.hash);
expect(block.parentHash).to.equal(previousBlock.hash);
}
});
}
});

View file

@ -0,0 +1,63 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { ConstantStore, deployCompiledContract, TransactionTypes } from "../../../../helpers";
describeSuite({
id: "D010103",
title: "Block gas limits",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let specVersion: number;
beforeAll(async () => {
specVersion = (await context.polkadotJs().runtimeVersion.specVersion).toNumber();
});
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: `${txnType} should be allowed to the max block gas`,
test: async () => {
const { hash, status } = await deployCompiledContract(context, "MultiplyBy7", {
type: txnType,
gas: ConstantStore(context).EXTRINSIC_GAS_LIMIT.get(specVersion)
});
expect(status).toBe("success");
const receipt = await context.viem().getTransactionReceipt({ hash });
expect(receipt.blockHash).toBeTruthy();
}
});
it({
id: `T0${TransactionTypes.indexOf(txnType) * 2 + 1}`,
title: `${txnType} should fail setting it over the max block gas`,
test: async () => {
await expect(async () =>
deployCompiledContract(context, "MultiplyBy7", {
type: txnType,
gas: ConstantStore(context).EXTRINSIC_GAS_LIMIT.get(specVersion) + 1n
})
).rejects.toThrowError();
}
});
}
it({
id: "T07",
title: "should be accessible within a contract",
test: async () => {
const { contractAddress, abi } = await deployCompiledContract(context, "BlockVariables", {
gas: 500_000n
});
const gasLimit = await context.viem().readContract({
address: contractAddress!,
abi,
args: [],
functionName: "getGasLimit"
});
expect(gasLimit).to.equal(ConstantStore(context).GAS_LIMIT.get(specVersion));
}
});
}
});

View file

@ -0,0 +1,65 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { ConstantStore } from "../../../../helpers";
describeSuite({
id: "D010104",
title: "Block genesis",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let specVersion: number;
beforeAll(async () => {
specVersion = (await context.polkadotJs().runtimeVersion.specVersion).toNumber();
});
it({
id: "T01",
title: "should contain block details",
test: async () => {
const block = await context.viem().getBlock({ blockNumber: 0n });
expect(block).to.include({
difficulty: 0n,
extraData: "0x",
gasLimit: ConstantStore(context).GAS_LIMIT.get(specVersion),
gasUsed: 0n,
logsBloom: `0x${"0".repeat(512)}`,
number: 0n,
receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
totalDifficulty: 0n,
transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
});
expect(block.transactions).to.be.an("array").that.is.empty;
expect(block.uncles).to.be.an("array").that.is.empty;
expect(block.nonce).to.equal("0x0000000000000000");
expect(block.hash).to.be.a("string").with.lengthOf(66);
expect(block.parentHash).to.be.a("string").with.lengthOf(66);
expect(block.timestamp).to.be.a("bigint");
}
});
it({
id: "T02",
title: "should be accessible by hash",
test: async () => {
const block = await context.viem().getBlock({ blockNumber: 0n });
const blockByHash = await context.viem().getBlock({ blockHash: block.hash! });
expect(blockByHash).to.include({
difficulty: 0n,
extraData: "0x",
gasLimit: ConstantStore(context).GAS_LIMIT.get(specVersion),
gasUsed: 0n,
logsBloom: `0x${"0".repeat(512)}`,
number: 0n,
receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
totalDifficulty: 0n,
transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
});
}
});
}
});

View file

@ -1,7 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/Moonsong-Labs/moonwall/main/packages/types/config_schema.json",
"label": "DataHaven Tests 🔷",
"defaultTestTimeout": 120000,
"defaultTestTimeout": 180000,
"scriptsDir": "scripts/",
"environments": [
{
"name": "dev_datahaven",
@ -12,7 +13,11 @@
"**/*test*.ts"
],
"timeout": 180000,
"multiThreads": 1,
"multiThreads": 4,
"contracts": "contracts/",
"runScripts": [
"compile-contracts.sh compile"
],
"envVars": [
"DEBUG_COLORS=1",
"RUST_BACKTRACE=1",
@ -41,37 +46,10 @@
"--no-grandpa",
"--no-prometheus",
"--sealing=manual"
],
"ports": {
"p2pPort": 30333,
"rpcPort": 9944
}
]
}
]
},
"connections": [
{
"name": "ethers",
"type": "ethers",
"endpoints": [
"ws://127.0.0.1:9944"
]
},
{
"name": "viem",
"type": "viem",
"endpoints": [
"ws://127.0.0.1:9944"
]
},
{
"name": "solo",
"type": "polkadotJs",
"endpoints": [
"ws://127.0.0.1:9944"
]
}
]
}
}
]
}

View file

@ -25,6 +25,7 @@
"stop:eth": "bun cli stop --enclave --no-datahaven --no-relayer",
"stop:engine": "bun cli stop --kurtosisEngine --no-datahaven --no-relayer --no-enclave",
"test:e2e": "bun test ./suites --timeout 900000",
"compile:contracts": "bun x tsx scripts/compile-contracts.ts compile",
"test:e2e:parallel": "bun scripts/test-parallel.ts",
"moonwall:test": "moonwall test dev_datahaven",
"moonwall:run": "moonwall run dev_datahaven",
@ -34,6 +35,7 @@
},
"devDependencies": {
"@types/bun": "latest",
"@types/yargs": "^17.0.33",
"@typescript/native-preview": "^7.0.0-dev.20250618.1"
},
"peerDependencies": {
@ -62,10 +64,12 @@
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"polkadot-api": "^1.15.1",
"solc": "^0.8.30",
"tiny-invariant": "^1.3.3",
"viem": "^2.31.3",
"wagmi": "^2.15.6",
"yaml": "^2.8.0",
"yargs": "^18.0.0",
"zod": "^3.25.67"
},
"trustedDependencies": [

View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bun x tsx "${SCRIPT_DIR}/compile-contracts.ts" "$@"

View file

@ -0,0 +1,305 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { CompiledContract } from "@moonwall/cli";
import chalk from "chalk";
import solc from "solc";
import type { Abi } from "viem";
import type { ArgumentsCamelCase } from "yargs";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
type CompileCommandOptions = {
PreCompilesDirectory: string;
OutputDirectory: string;
SourceDirectory: string;
Verbose: boolean;
};
const sourceByReference: Record<string, string> = {};
const countByReference: Record<string, number> = {};
const refByContract: Record<string, string> = {};
let contractMd5: Record<string, string> = {};
const solcVersion = solc.version();
yargs(hideBin(process.argv))
.usage("Usage: $0")
.version("2.0.0")
.options({
PreCompilesDirectory: {
type: "string",
alias: "p",
description: "Path to directory containing precompile solidity files",
default: "../operator/precompiles/"
},
OutputDirectory: {
type: "string",
alias: "o",
description: "Output directory for compiled contracts",
default: "datahaven/contracts/out"
},
SourceDirectory: {
type: "string",
alias: "i",
description: "Source directory for solidity contracts to compile",
default: "datahaven/contracts/src"
},
Verbose: {
type: "boolean",
alias: "v",
description: "Verbose mode for extra logging.",
default: false
}
})
.command<CompileCommandOptions>({
command: "compile",
describe: "Compile contracts",
handler: async (argv) => {
await main(argv);
}
})
.parse();
async function main(args: ArgumentsCamelCase<CompileCommandOptions>) {
const precompilesPath = path.join(process.cwd(), args.PreCompilesDirectory);
const outputDirectory = path.join(process.cwd(), args.OutputDirectory);
const sourceDirectory = path.join(process.cwd(), args.SourceDirectory);
const tempFile = path.join(process.cwd(), args.OutputDirectory, ".compile.tmp");
console.log(`🧪 Solc version: ${solcVersion}`);
const precompilesDirectoryExists = await fs
.access(precompilesPath)
.then(() => true)
.catch(() => false);
if (precompilesDirectoryExists) {
console.log(`🗃️ Precompiles directory: ${precompilesPath}`);
} else {
console.log(`⚠️ Precompiles directory not found (${precompilesPath}), skipping.`);
}
console.log(`🗃️ Output directory: ${outputDirectory}`);
console.log(`🗃️ Source directory: ${sourceDirectory}`);
await fs.mkdir(outputDirectory, { recursive: true });
// Order is important so precompiles are available first
const contractSourcePaths: Array<{
filepath: string;
importPath: string;
compile: boolean;
}> = [];
if (precompilesDirectoryExists) {
const entries = await fs.readdir(precompilesPath);
for (const entry of entries) {
const fullPath = path.join(precompilesPath, entry);
contractSourcePaths.push({
filepath: fullPath,
importPath: path.posix.join("precompiles", entry),
compile: true
});
}
}
contractSourcePaths.push({
filepath: sourceDirectory,
importPath: "", // Reference in contracts are local
compile: true
});
const sourceToCompile: Record<string, string> = {};
const filePaths: string[] = [];
for (const contractPath of contractSourcePaths) {
const contracts = (await getFiles(contractPath.filepath)).filter((filename: string) =>
filename.endsWith(".sol")
);
for (const filepath of contracts) {
const relativePath = path.relative(contractPath.filepath, filepath);
const normalizedRelative = relativePath.split(path.sep).join(path.posix.sep);
const importPrefix = contractPath.importPath.replace(/\/$/, "");
const ref = importPrefix
? `${importPrefix}/${normalizedRelative}`.replace(/^\/+/, "")
: normalizedRelative.replace(/^\/+/, "");
filePaths.push(filepath);
sourceByReference[ref] = (await fs.readFile(filepath)).toString();
if (contractPath.compile) {
countByReference[ref] = 0;
if (!sourceByReference[ref].includes("// skip-compilation")) {
sourceToCompile[ref] = sourceByReference[ref];
}
}
}
}
console.log(`📁 Found ${Object.keys(sourceToCompile).length} contracts to compile`);
const contractsToCompile: string[] = [];
const tempFileExists = await fs
.access(tempFile)
.then(() => true)
.catch(() => false);
if (tempFileExists) {
contractMd5 = JSON.parse((await fs.readFile(tempFile)).toString()) as Record<string, string>;
for (const contract of Object.keys(sourceToCompile)) {
const filePath = filePaths.find((candidate) => candidate.includes(contract));
if (!filePath) {
continue;
}
const contractHash = computeHash((await fs.readFile(filePath)).toString());
if (contractHash !== contractMd5[contract]) {
console.log(` - Change in ${chalk.yellow(contract)}, compiling ⚙️`);
contractsToCompile.push(contract);
} else if (args.Verbose) {
console.log(` - No change to ${chalk.green(contract)}, skipping ✅`);
}
}
} else {
console.log(` - ${chalk.yellow("No temp file found, compiling all contracts ⚙️")}`);
contractsToCompile.push(...Object.keys(sourceToCompile));
}
// Compile contracts
for (const ref of contractsToCompile) {
try {
await compile(ref, outputDirectory, tempFile);
await fs.writeFile(tempFile, JSON.stringify(contractMd5, null, 2));
} catch (e: any) {
console.log(`Failed to compile: ${ref}`);
if (e.errors) {
for (const error of e.errors) {
console.log(error.formattedMessage);
}
} else {
console.log(e);
}
process.exit(1);
}
}
// for (const ref of Object.keys(countByReference)) {
// if (!countByReference[ref]) {
// console.log(`${chalk.red("Warning")}: ${ref} never used: ${countByReference[ref]}`);
// }
// }
}
// For some reasons, solc doesn't provide the relative path to imports :(
const getImports = (fileRef: string) => (dependency: string) => {
if (sourceByReference[dependency]) {
countByReference[dependency] = (countByReference[dependency] || 0) + 1;
return { contents: sourceByReference[dependency] };
}
let base = fileRef;
while (base && base.length > 1) {
const localRef = path.join(base, dependency);
if (sourceByReference[localRef]) {
countByReference[localRef] = (countByReference[localRef] || 0) + 1;
return { contents: sourceByReference[localRef] };
}
base = path.dirname(base);
}
return { error: "Source not found" };
};
function compileSolidity(
fileRef: string,
contractContent: string
): { [name: string]: CompiledContract<Abi> } {
const filename = path.basename(fileRef);
const result = JSON.parse(
solc.compile(
JSON.stringify({
language: "Solidity",
sources: {
[filename]: {
content: contractContent
}
},
settings: {
optimizer: { enabled: true, runs: 200 },
outputSelection: {
"*": {
"*": ["*"]
}
},
debug: {
revertStrings: "debug"
}
}
}),
{ import: getImports(fileRef) }
)
);
if (!result.contracts) {
throw result;
}
return Object.keys(result.contracts[filename]).reduce(
(p, contractName) => {
p[contractName] = {
byteCode:
`0x${result.contracts[filename][contractName].evm.bytecode.object}` as `0x${string}`,
contract: result.contracts[filename][contractName],
sourceCode: contractContent
};
return p;
},
{} as { [name: string]: CompiledContract<Abi> }
);
}
// Shouldn't be run concurrently with the same 'name'
async function compile(
fileRef: string,
destPath: string,
tempFilePath: string
): Promise<{ [name: string]: CompiledContract<Abi> }> {
const soliditySource = sourceByReference[fileRef];
countByReference[fileRef]++;
if (!soliditySource) {
throw new Error(`Missing solidity file: ${fileRef}`);
}
const compiledContracts = compileSolidity(fileRef, soliditySource);
await Promise.all(
Object.keys(compiledContracts).map(async (contractName) => {
const dest = `${path.join(destPath, path.dirname(fileRef), contractName)}.json`;
if (refByContract[dest]) {
console.warn(
chalk.red(
`Contract ${contractName} already exist from ${refByContract[dest]}. Erasing previous version`
)
);
}
await fs.mkdir(path.dirname(dest), { recursive: true });
await fs.writeFile(dest, JSON.stringify(compiledContracts[contractName], null, 2), {
flag: "w",
encoding: "utf-8"
});
console.log(` - ${chalk.green(`${contractName}.json`)} file has been saved 💾`);
refByContract[dest] = fileRef;
contractMd5[fileRef] = computeHash(soliditySource);
})
);
await fs.mkdir(path.dirname(tempFilePath), { recursive: true });
return compiledContracts;
}
async function getFiles(dir: string): Promise<string[]> {
const subdirs = await fs.readdir(dir);
const files = await Promise.all(
subdirs.map(async (subdir): Promise<string[]> => {
const resolvedPath = path.resolve(dir, subdir);
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) {
return getFiles(resolvedPath);
}
return [resolvedPath];
})
);
return files.flat();
}
function computeHash(input: string): string {
const hash = crypto.createHash("md5");
hash.update(input + solcVersion);
return hash.digest("hex");
}