From f040682d937cfc5b64a05aaa2fe79c0ea3f2f865 Mon Sep 17 00:00:00 2001 From: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:47:35 +0200 Subject: [PATCH] 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 --- test/bun.lock | 21 ++ .../contracts/src/BlockVariables.sol | 26 ++ test/datahaven/contracts/src/Fibonacci.sol | 18 ++ test/datahaven/contracts/src/MultiplyBy7.sol | 8 + test/datahaven/helpers/constants.ts | 16 +- test/datahaven/helpers/contracts.ts | 64 ++++ test/datahaven/helpers/deploy.ts | 66 ++++ test/datahaven/helpers/index.ts | 1 + .../test-block/test-block-1.ts} | 2 +- .../dev/common/test-block/test-block-2.ts | 36 +++ .../dev/common/test-block/test-block-gas.ts | 63 ++++ .../common/test-block/test-block-genesis.ts | 65 ++++ test/moonwall.config.json | 40 +-- test/package.json | 4 + test/scripts/compile-contracts.sh | 8 + test/scripts/compile-contracts.ts | 305 ++++++++++++++++++ 16 files changed, 708 insertions(+), 35 deletions(-) create mode 100644 test/datahaven/contracts/src/BlockVariables.sol create mode 100644 test/datahaven/contracts/src/Fibonacci.sol create mode 100644 test/datahaven/contracts/src/MultiplyBy7.sol create mode 100644 test/datahaven/helpers/contracts.ts create mode 100644 test/datahaven/helpers/deploy.ts rename test/datahaven/suites/dev/{test-block.ts => common/test-block/test-block-1.ts} (98%) create mode 100644 test/datahaven/suites/dev/common/test-block/test-block-2.ts create mode 100644 test/datahaven/suites/dev/common/test-block/test-block-gas.ts create mode 100644 test/datahaven/suites/dev/common/test-block/test-block-genesis.ts create mode 100755 test/scripts/compile-contracts.sh create mode 100644 test/scripts/compile-contracts.ts diff --git a/test/bun.lock b/test/bun.lock index 6cf30c28..c170f78e 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -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=="], diff --git a/test/datahaven/contracts/src/BlockVariables.sol b/test/datahaven/contracts/src/BlockVariables.sol new file mode 100644 index 00000000..af4f08c0 --- /dev/null +++ b/test/datahaven/contracts/src/BlockVariables.sol @@ -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; + } +} diff --git a/test/datahaven/contracts/src/Fibonacci.sol b/test/datahaven/contracts/src/Fibonacci.sol new file mode 100644 index 00000000..cf24c7e1 --- /dev/null +++ b/test/datahaven/contracts/src/Fibonacci.sol @@ -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; + } +} diff --git a/test/datahaven/contracts/src/MultiplyBy7.sol b/test/datahaven/contracts/src/MultiplyBy7.sol new file mode 100644 index 00000000..6c99fc75 --- /dev/null +++ b/test/datahaven/contracts/src/MultiplyBy7.sol @@ -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; + } +} diff --git a/test/datahaven/helpers/constants.ts b/test/datahaven/helpers/constants.ts index 3795476b..575e38ec 100644 --- a/test/datahaven/helpers/constants.ts +++ b/test/datahaven/helpers/constants.ts @@ -32,15 +32,25 @@ class RuntimeConstant { } // 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) } }; diff --git a/test/datahaven/helpers/contracts.ts b/test/datahaven/helpers/contracts.ts new file mode 100644 index 00000000..130d59f5 --- /dev/null +++ b/test/datahaven/helpers/contracts.ts @@ -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 => { + 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; +}; diff --git a/test/datahaven/helpers/deploy.ts b/test/datahaven/helpers/deploy.ts new file mode 100644 index 00000000..dcfce442 --- /dev/null +++ b/test/datahaven/helpers/deploy.ts @@ -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 => { + 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 + }; +}; diff --git a/test/datahaven/helpers/index.ts b/test/datahaven/helpers/index.ts index b04bfcf7..5feb05cc 100644 --- a/test/datahaven/helpers/index.ts +++ b/test/datahaven/helpers/index.ts @@ -1 +1,2 @@ export * from "./constants"; +export * from "./deploy"; diff --git a/test/datahaven/suites/dev/test-block.ts b/test/datahaven/suites/dev/common/test-block/test-block-1.ts similarity index 98% rename from test/datahaven/suites/dev/test-block.ts rename to test/datahaven/suites/dev/common/test-block/test-block-1.ts index f7f01078..2250ee2a 100644 --- a/test/datahaven/suites/dev/test-block.ts +++ b/test/datahaven/suites/dev/common/test-block/test-block-1.ts @@ -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", diff --git a/test/datahaven/suites/dev/common/test-block/test-block-2.ts b/test/datahaven/suites/dev/common/test-block/test-block-2.ts new file mode 100644 index 00000000..9c70b105 --- /dev/null +++ b/test/datahaven/suites/dev/common/test-block/test-block-2.ts @@ -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); + } + }); + } +}); diff --git a/test/datahaven/suites/dev/common/test-block/test-block-gas.ts b/test/datahaven/suites/dev/common/test-block/test-block-gas.ts new file mode 100644 index 00000000..f4fa6154 --- /dev/null +++ b/test/datahaven/suites/dev/common/test-block/test-block-gas.ts @@ -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)); + } + }); + } +}); diff --git a/test/datahaven/suites/dev/common/test-block/test-block-genesis.ts b/test/datahaven/suites/dev/common/test-block/test-block-genesis.ts new file mode 100644 index 00000000..88cd9d96 --- /dev/null +++ b/test/datahaven/suites/dev/common/test-block/test-block-genesis.ts @@ -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" + }); + } + }); + } +}); diff --git a/test/moonwall.config.json b/test/moonwall.config.json index 68827753..322ff771 100644 --- a/test/moonwall.config.json +++ b/test/moonwall.config.json @@ -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" - ] - } - ] + } } ] } \ No newline at end of file diff --git a/test/package.json b/test/package.json index 6cde60b4..ef087255 100644 --- a/test/package.json +++ b/test/package.json @@ -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": [ diff --git a/test/scripts/compile-contracts.sh b/test/scripts/compile-contracts.sh new file mode 100755 index 00000000..b154e4a2 --- /dev/null +++ b/test/scripts/compile-contracts.sh @@ -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" "$@" + diff --git a/test/scripts/compile-contracts.ts b/test/scripts/compile-contracts.ts new file mode 100644 index 00000000..755e6671 --- /dev/null +++ b/test/scripts/compile-contracts.ts @@ -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 = {}; +const countByReference: Record = {}; +const refByContract: Record = {}; +let contractMd5: Record = {}; +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({ + command: "compile", + describe: "Compile contracts", + handler: async (argv) => { + await main(argv); + } + }) + .parse(); + +async function main(args: ArgumentsCamelCase) { + 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 = {}; + 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; + 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 } { + 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 } + ); +} + +// Shouldn't be run concurrently with the same 'name' +async function compile( + fileRef: string, + destPath: string, + tempFilePath: string +): Promise<{ [name: string]: CompiledContract }> { + 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 { + const subdirs = await fs.readdir(dir); + const files = await Promise.all( + subdirs.map(async (subdir): Promise => { + 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"); +}