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

Frontier Template: PoV Handling #716

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion client/consensus/src/manual_seal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

//! The Manual Seal implementation for the OrchestratorAuraConsensus

use sp_api::StorageProof;
use {
cumulus_primitives_core::ParaId,
dp_consensus::TanssiAuthorityAssignmentApi,
Expand Down Expand Up @@ -175,7 +176,7 @@ impl<B> ConsensusDataProvider<B> for ContainerManualSealAuraConsensusDataProvide
where
B: BlockT,
{
type Proof = ();
type Proof = StorageProof;

fn create_digest(
&self,
Expand Down
5 changes: 3 additions & 2 deletions client/node-common/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ use {

#[allow(deprecated)]
use sc_executor::NativeElseWasmExecutor;
use sp_api::StorageProof;

/// Trait to configure the main types the builder rely on, bundled in a single
/// type to reduce verbosity and the amount of type parameters.
Expand Down Expand Up @@ -625,7 +626,7 @@ where

let prometheus_registry = self.prometheus_registry.clone();

let mut env = sc_basic_authorship::ProposerFactory::new(
let mut env = sc_basic_authorship::ProposerFactory::with_proof_recording(
self.task_manager.spawn_handle(),
self.client.clone(),
self.transaction_pool.clone(),
Expand Down Expand Up @@ -953,6 +954,6 @@ pub struct ManualSealConfiguration<B, BI, SC, CIDP> {
pub block_import: BI,
pub soft_deadline: Option<Percent>,
pub select_chain: SC,
pub consensus_data_provider: Option<Box<dyn ConsensusDataProvider<B, Proof = ()>>>,
pub consensus_data_provider: Option<Box<dyn ConsensusDataProvider<B, Proof = StorageProof>>>,
pub create_inherent_data_providers: CIDP,
}
44 changes: 39 additions & 5 deletions container-chains/runtime-templates/frontier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ parameter_types! {
pub PrecompilesValue: TemplatePrecompiles<Runtime> = TemplatePrecompiles::<_>::new();
pub WeightPerGas: Weight = Weight::from_parts(WEIGHT_PER_GAS, 0);
pub SuicideQuickClearLimit: u32 = 0;
pub GasLimitPovSizeRatio: u32 = 16;
}

impl_on_charge_evm_transaction!();
Expand Down Expand Up @@ -870,8 +871,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 SuicideQuickClearLimit = SuicideQuickClearLimit;
type Timestamp = Timestamp;
type WeightInfo = ();
Expand Down Expand Up @@ -1519,20 +1519,54 @@ impl_runtime_apis! {
) -> Result<pallet_evm::CallInfo, sp_runtime::DispatchError> {
let is_transactional = false;
let validate = true;

// Estimated encoded transaction size must be based on the heaviest transaction
// type (EIP1559Transaction) to be compatible with all transaction types.
let mut estimated_transaction_len = data.len() +
// pallet ethereum index: 1
// transact call index: 1
// Transaction enum variant: 1
// chain_id 8 bytes
// nonce: 32
// max_priority_fee_per_gas: 32
// max_fee_per_gas: 32
// gas_limit: 32
// action: 21 (enum varianrt + call address)
// value: 32
// access_list: 1 (empty vec size)
// 65 bytes signature
258;

if access_list.is_some() {
estimated_transaction_len += access_list.encoded_size();
}
let gas_limit = gas_limit.min(u64::MAX.into()).low_u64();
let without_base_extrinsic_weight = true;
let (weight_limit, proof_size_base_cost) =
match <Runtime as pallet_evm::Config>::GasWeightMapping::gas_to_weight(
gas_limit,
without_base_extrinsic_weight
) {
weight_limit if weight_limit.proof_size() > 0 => {
(Some(weight_limit), Some(estimated_transaction_len as u64))
}
_ => (None, None),
};

<Runtime as pallet_evm::Config>::Runner::call(
from,
to,
data,
value,
gas_limit.min(u64::MAX.into()).low_u64(),
gas_limit,
max_fee_per_gas,
max_priority_fee_per_gas,
nonce,
access_list.unwrap_or_default(),
is_transactional,
validate,
None,
None,
weight_limit,
proof_size_base_cost,
<Runtime as pallet_evm::Config>::config(),
).map_err(|err| err.error.into())
}
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);
}
}
64 changes: 64 additions & 0 deletions test/helpers/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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[];
};
1 change: 1 addition & 0 deletions test/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./eth-transactions";
export * from "./xcm";
export * from "./contracts";
107 changes: 107 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,107 @@
import "@tanssi/api-augment";
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, createEthersTransaction } from "@moonwall/util";
import { Abi, encodeFunctionData } from "viem";
import { expectEVMResult, HeavyContract, deployHeavyContracts } from "../../../helpers";

describeSuite({
id: "DF1301",
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 = 16_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})`);
expect(block.proofSize).toBeGreaterThanOrEqual(EXPECTED_POV_ROUGH / 1.2);
expect(block.proofSize).toBeLessThanOrEqual(EXPECTED_POV_ROUGH * 1.2);
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: 12_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 / 1.2);
expect(block.proofSize).to.be.at.most(EXPECTED_POV_ROUGH * 1.2);
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: 100_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 ~38k of PoV, so we verify we are within range.
expect(block.proofSize).to.be.at.least(15_000);
expect(block.proofSize).to.be.at.most(25_000);
expect(result?.successful).to.equal(true);
expectEVMResult(result!.events, "Error", "OutOfGas");
},
});
},
});
Loading
Loading