Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set PoV in frontier template #225

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions container-chains/templates/frontier/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ impl FindAuthor<H160> for FindAuthorAdapter {

parameter_types! {
pub BlockGasLimit: U256 = U256::from(BLOCK_GAS_LIMIT);
pub const GasLimitPovSizeRatio: u64 = 4;
pub PrecompilesValue: FrontierPrecompiles<Runtime> = FrontierPrecompiles::<_>::new();
pub WeightPerGas: Weight = Weight::from_parts(weight_per_gas(BLOCK_GAS_LIMIT, NORMAL_DISPATCH_RATIO, WEIGHT_MILLISECS_PER_BLOCK), 0);
}
Expand All @@ -804,8 +805,7 @@ impl pallet_evm::Config for Runtime {
type OnChargeTransaction = OnChargeEVMTransaction<()>;
type OnCreate = ();
type FindAuthor = FindAuthorAdapter;
// TODO: update in the future
type GasLimitPovSizeRatio = ();
type GasLimitPovSizeRatio = GasLimitPovSizeRatio;
type Timestamp = Timestamp;
type WeightInfo = ();
}
Expand Down
26 changes: 26 additions & 0 deletions test/contracts/solidity/CallForwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

contract CallForwarder {
function call(
address target,
bytes memory data
) public returns (bool, bytes memory) {
return target.call(data);
}

function callRange(address first, address last) public {
require(first < last, "invalid range");
while (first < last) {
first.call("");
first = address(uint160(first) + 1);
}
}

function delegateCall(
address target,
bytes memory data
) public returns (bool, bytes memory) {
return target.delegatecall(data);
}
}
63 changes: 63 additions & 0 deletions test/helpers/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DevModeContext } from "@moonwall/cli";
import { ALITH_ADDRESS, alith } from "@moonwall/util";

export interface HeavyContract {
deployed: boolean;
account: string;
key: string;
}
/**
* @description Deploy multiple contracts to test the EVM storage limit.
* @param context Context of the test
* @param count Number of contracts to deploy
* @returns
*/
export const deployHeavyContracts = async (context: DevModeContext, first = 6000, last = 6999) => {
// Generate the contract addresses
const contracts = await Promise.all(
new Array(last - first + 1).fill(0).map(async (_, i) => {
const account = `0x${(i + first).toString(16).padStart(40, "0")}`;
return {
deployed: false,
account,
key: context.polkadotJs().query.evm.accountCodes.key(account),
};
})
);

// Check which contracts are already deployed
for (const contract of contracts) {
contract.deployed = (await context.polkadotJs().rpc.state.getStorage(contract.key))!.toString().length > 10;
}

// Create the contract code (24kb of zeros)
const evmCode = `60006000fd${"0".repeat(24_000 * 2)}`;
const storageData = `${context
.polkadotJs()
.registry.createType("Compact<u32>", `0x${BigInt((evmCode.length + 1) * 2).toString(16)}`)
.toHex(true)}${evmCode}`;

// Create the batchs of contracts to deploy
const batchs = contracts
.reduce(
(acc, value) => {
if (acc[acc.length - 1].length >= 30) acc.push([]);
if (!value.deployed) acc[acc.length - 1].push([value.key, storageData]);
return acc;
},
[[]] as [string, string][][]
)
.filter((batch) => batch.length > 0);

// Set the storage of the contracts
let nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
for (let i = 0; i < batchs.length; i++) {
const batch = batchs[i];
await context.createBlock([
context.polkadotJs().tx.sudo.sudo(context.polkadotJs().tx.system.setStorage(batch)).signAsync(alith, {
nonce: nonce++,
}),
]);
}
return contracts as HeavyContract[];
};
68 changes: 68 additions & 0 deletions test/helpers/eth-transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import "@moonbeam-network/api-augment";
import { DevModeContext, expect } from "@moonwall/cli";
import { EventRecord } from "@polkadot/types/interfaces";
import {
EvmCoreErrorExitError,
EvmCoreErrorExitFatal,
EvmCoreErrorExitReason,
EvmCoreErrorExitRevert,
EvmCoreErrorExitSucceed,
} from "@polkadot/types/lookup";
export type Errors = {
Succeed: EvmCoreErrorExitSucceed["type"];
Error: EvmCoreErrorExitError["type"];
Revert: EvmCoreErrorExitRevert["type"];
Fatal: EvmCoreErrorExitFatal["type"];
};

export async function extractRevertReason(context: DevModeContext, responseHash: string) {
const tx = (await context.ethers().provider!.getTransaction(responseHash))!;
try {
await context.ethers().call({ to: tx.to, data: tx.data, gasLimit: tx.gasLimit });
return null;
} catch (e: any) {
const errorMessage = e.info.error.message;
return errorMessage.split("VM Exception while processing transaction: revert ")[1];
}
}

export function expectEVMResult<T extends Errors, Type extends keyof T>(
events: EventRecord[],
resultType: Type,
reason?: T[Type]
) {
expect(events, `Missing events, probably failed execution`).to.be.length.at.least(1);
const ethereumResult = events.find(
({ event: { section, method } }) => section == "ethereum" && method == "Executed"
)!.event.data[3] as EvmCoreErrorExitReason;

const foundReason = ethereumResult.isError
? ethereumResult.asError.type
: ethereumResult.isFatal
? ethereumResult.asFatal.type
: ethereumResult.isRevert
? ethereumResult.asRevert.type
: ethereumResult.asSucceed.type;

expect(ethereumResult.type, `Invalid EVM Execution - (${ethereumResult.type}.${foundReason})`).to.equal(resultType);
if (reason) {
if (ethereumResult.isError) {
expect(ethereumResult.asError.type, `Invalid EVM Execution ${ethereumResult.type} Reason`).to.equal(reason);
} else if (ethereumResult.isFatal) {
expect(ethereumResult.asFatal.type, `Invalid EVM Execution ${ethereumResult.type} Reason`).to.equal(reason);
} else if (ethereumResult.isRevert) {
expect(ethereumResult.asRevert.type, `Invalid EVM Execution ${ethereumResult.type} Reason`).to.equal(
reason
);
} else
expect(ethereumResult.asSucceed.type, `Invalid EVM Execution ${ethereumResult.type} Reason`).to.equal(
reason
);
}
}

export async function getTransactionFees(context: DevModeContext, hash: string): Promise<bigint> {
const receipt = await context.viem().getTransactionReceipt({ hash: hash as `0x${string}` });

return receipt.gasUsed * receipt.effectiveGasPrice;
}
89 changes: 89 additions & 0 deletions test/helpers/expect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BlockCreationResponse, expect } from "@moonwall/cli";
import type { EventRecord } from "@polkadot/types/interfaces";
import { ApiTypes, AugmentedEvent, AugmentedEvents, SubmittableExtrinsic } from "@polkadot/api/types";
import { IEvent } from "@polkadot/types/types";

export type ExtractTuple<P> = P extends AugmentedEvent<"rxjs", infer T> ? T : never;

export async function expectOk<
ApiType extends ApiTypes,
Call extends SubmittableExtrinsic<ApiType> | Promise<SubmittableExtrinsic<ApiType>> | string | Promise<string>,
Calls extends Call | Call[],
BlockCreation extends BlockCreationResponse<ApiType, Calls extends Call[] ? Awaited<Call>[] : Awaited<Call>>
>(call: Promise<BlockCreation>): Promise<BlockCreation> {
const block = await call;
if (Array.isArray(block.result)) {
block.result.forEach((r, idx) => {
expect(
r.successful,
`tx[${idx}] - ${r.error?.name}${
r.extrinsic
? `\n\t\t${r.extrinsic.method.section}.${r.extrinsic.method.method}(${r.extrinsic.args
.map((d) => d.toHuman())
.join("; ")})`
: ""
}`
).to.be.true;
});
} else {
expect(block.result.successful, block.result.error?.name).to.be.true;
}
return block;
}

export function expectSubstrateEvent<
ApiType extends ApiTypes,
Call extends SubmittableExtrinsic<ApiType> | Promise<SubmittableExtrinsic<ApiType>> | string | Promise<string>,
Calls extends Call | Call[],
Event extends AugmentedEvents<ApiType>,
Section extends keyof Event,
Method extends keyof Event[Section],
Tuple extends ExtractTuple<Event[Section][Method]>
>(
block: BlockCreationResponse<ApiType, Calls extends Call[] ? Awaited<Call>[] : Awaited<Call>>,
section: Section,
method: Method
): IEvent<Tuple> {
let event: EventRecord | undefined;
if (Array.isArray(block.result)) {
block.result.forEach((r) => {
const foundEvents = r.events.filter(
({ event }) => event.section.toString() == section && event.method.toString() == method
);
if (foundEvents.length > 0) {
expect(
event,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).toBeUndefined();
expect(
foundEvents,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).to.be.length(1);
event = foundEvents[0];
}
});
} else {
const foundEvents = block.result!.events!.filter(
({ event }) => event.section.toString() == section && event.method.toString() == method
);
if (foundEvents.length > 0) {
expect(
foundEvents,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).to.be.length(1);
event = foundEvents[0];
}
}
expect(
event,
`Event ${section.toString()}.${method.toString()} not found:\n${(Array.isArray(block.result)
? block.result.map((r) => r.events).flat()
: block.result
? block.result.events
: []
)
.map(({ event }) => ` - ${event.section.toString()}.${event.method.toString()}\n`)
.join("")}`
).to.not.be.undefined;
return event!.event as any;
}
109 changes: 109 additions & 0 deletions test/suites/dev-frontier-template/test-pov/test-evm-over-pov.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import "@moonbeam-network/api-augment";
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, createEthersTransaction } from "@moonwall/util";
import { Abi, encodeFunctionData } from "viem";
import { expectEVMResult } from "../../../helpers/eth-transactions.js";
import { HeavyContract, deployHeavyContracts } from "../../../helpers/contracts.js";

describeSuite({
id: "D2401",
title: "PoV controlled by gasLimit",
foundationMethods: "dev",
testCases: ({ context, it, log }) => {
let proxyAddress: `0x${string}`;
let proxyAbi: Abi;
let contracts: HeavyContract[];
let callData: `0x${string}`;
const MAX_CONTRACTS = 20;
const EXPECTED_POV_ROUGH = 50_000; // bytes

beforeAll(async () => {
const { contractAddress, abi } = await deployCreateCompiledContract(context, "CallForwarder");
proxyAddress = contractAddress;
proxyAbi = abi;

// Deploy heavy contracts (test won't use more than what is needed for reaching max pov)
contracts = await deployHeavyContracts(context, 6000, 6000 + MAX_CONTRACTS);

callData = encodeFunctionData({
abi: proxyAbi,
functionName: "callRange",
args: [contracts[0].account, contracts[MAX_CONTRACTS].account],
});
});

it({
id: "T01",
title: "should allow to include transaction with estimate gas to cover PoV",
test: async function () {
const gasEstimate = await context.viem().estimateGas({
account: ALITH_ADDRESS,
to: proxyAddress,
value: 0n,
data: callData,
});

const rawSigned = await createEthersTransaction(context, {
to: proxyAddress,
data: callData,
txnType: "eip1559",
gasLimit: gasEstimate,
});

const { result, block } = await context.createBlock(rawSigned);

log(`block.proofSize: ${block.proofSize} (successful: ${result?.successful})`);
console.log(block);
expect(block.proofSize).toBeGreaterThanOrEqual(EXPECTED_POV_ROUGH / 2.0);
expect(block.proofSize).toBeLessThanOrEqual(EXPECTED_POV_ROUGH * 1.1);
expect(result?.successful).to.equal(true);
},
});

it({
id: "T02",
title: "should allow to include transaction with enough gas limit to cover PoV",
test: async function () {
const rawSigned = await createEthersTransaction(context, {
to: proxyAddress,
data: callData,
txnType: "eip1559",
gasLimit: 3_000_000,
});

const { result, block } = await context.createBlock(rawSigned);

log(`block.proof_size: ${block.proofSize} (successful: ${result?.successful})`);
expect(block.proofSize).to.be.at.least(EXPECTED_POV_ROUGH / 2.0);
expect(block.proofSize).to.be.at.most(EXPECTED_POV_ROUGH * 1.1);
expect(result?.successful).to.equal(true);
},
});

it({
id: "T03",
title: "should fail to include transaction without enough gas limit to cover PoV",
test: async function () {
// This execution uses only < 100k Gas in cpu execute but require 2M Gas for PoV.
// We are providing only 1M Gas, so it should fail.
const rawSigned = await createEthersTransaction(context, {
to: proxyAddress,
data: callData,
txnType: "eip1559",
gasLimit: 1_000_000,
});

const { result, block } = await context.createBlock(rawSigned);

log(`block.proof_size: ${block.proofSize} (successful: ${result?.successful})`);
// The block still contain the failed (out of gas) transaction so the PoV is still included
// in the block.
// 1M Gas allows ~250k of PoV, so we verify we are within range.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is specially suspicious, 1M gas should indeed account for 250K pov proof, and our tests clearly indicate they dont

expect(block.proofSize).to.be.at.least(23_000);
expect(block.proofSize).to.be.at.most(50_000);
expect(result?.successful).to.equal(true);
expectEVMResult(result!.events, "Error", "OutOfGas");
},
});
},
});
Loading