mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
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:
parent
dc0f0673e2
commit
f040682d93
16 changed files with 708 additions and 35 deletions
|
|
@ -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=="],
|
||||
|
|
|
|||
26
test/datahaven/contracts/src/BlockVariables.sol
Normal file
26
test/datahaven/contracts/src/BlockVariables.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
18
test/datahaven/contracts/src/Fibonacci.sol
Normal file
18
test/datahaven/contracts/src/Fibonacci.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
8
test/datahaven/contracts/src/MultiplyBy7.sol
Normal file
8
test/datahaven/contracts/src/MultiplyBy7.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
64
test/datahaven/helpers/contracts.ts
Normal file
64
test/datahaven/helpers/contracts.ts
Normal 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;
|
||||
};
|
||||
66
test/datahaven/helpers/deploy.ts
Normal file
66
test/datahaven/helpers/deploy.ts
Normal 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
|
||||
};
|
||||
};
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from "./constants";
|
||||
export * from "./deploy";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
36
test/datahaven/suites/dev/common/test-block/test-block-2.ts
Normal file
36
test/datahaven/suites/dev/common/test-block/test-block-2.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
8
test/scripts/compile-contracts.sh
Executable file
8
test/scripts/compile-contracts.sh
Executable 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" "$@"
|
||||
|
||||
305
test/scripts/compile-contracts.ts
Normal file
305
test/scripts/compile-contracts.ts
Normal 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");
|
||||
}
|
||||
Loading…
Reference in a new issue